manateelazycat / popweb

Show popup web window for Emacs
146 stars 17 forks source link

支持对 Anki note review,阅读外文时随时 review Anki 积累的语料库 #57

Closed czqhurricnae closed 1 year ago

czqhurricnae commented 1 year ago

刚发现,popweb-anki-review-show 命令在运行成功一次后,在 popweb 窗口 hide 后,总是出现:

Process *popweb* segmentation fault: 11

在运行 popweb-anki-review-show 后 popweb 窗口无法 popup。需要重启 popweb。

经过排查,只有将:

    web_window.webview.channel.registerObject('handler', web_window.webview.handler)
    web_window.webview.page().setWebChannel(web_window.webview.channel)

https://github.com/czqhurricnae/popweb/blob/47133d309c50a3860966f40a8b02b1ae6a851f2d/extension/anki-review/popweb-anki-review.py#L277-L278

注释掉,该错误才不会出现。

但是这样就无法使得 popweb 的 Python 与 JS 进行通信。

是不是应该在 popweb 窗口 hide 同时对通信用的 channel 进行处理?

但是怎么处理我不懂,google 也没能找到。

manateelazycat commented 1 year ago

因为子线程调用qt图形代码了,应该用@postGui 包装一下函数

czqhurricnae commented 1 year ago

我尝试用 @postGui 装饰器包装了,pop_anki_review_window,expanded_on_bridge_cmd。都没有效果。


def pop_anki_review_window(popweb, module_name, index_file, x, y,
                           x_offset, y_offset, frame_x, frame_y, frame_w, frame_h,
                           width_scale, height_scale, show_window,
                           script_file, media_directory,
                           new_query_p, emacs_query):

    web_window = popweb.get_web_window(module_name)
    window_width = frame_w * width_scale
    window_height = frame_h * height_scale
    window_x, window_y = popweb.adjust_render_pos(x + x_offset, y + y_offset,
                                                  x_offset, y_offset,
                                                  window_width, window_height,
                                                  frame_x, frame_y,
                                                  frame_w, frame_h)

    class CallHandler(QObject):
        @pyqtSlot(QVariant, result=QVariant)
        def expanded_on_bridge_cmd(self, cmd: str) -> Tuple[bool, Any]:

            if cmd.startswith("rev-search "):
                tooltip_id = int(cmd.split()[1])
                query      = " ".join(cmd.split()[2:])
                if (query.isascii() and len(query) >= 3) or (not query.isascii() and len(query) >= 2):
                    notes = get_notes(query)
                    web_window.web_page.runJavaScript(f"setTooltipSearchResults({tooltip_id}, {json.dumps(notes)})")
                return (True, None)

            elif cmd.startswith("rev-tt-edit "):
                invoke("guiEditNote", note=int(cmd.split()[1]))
                return (True, None)

            elif cmd.startswith("rev-tt-playsound "):
                tooltip_id = int(cmd.split()[1])
                note       = invoke("notesInfo", notes=[tooltip_id])
                results    = re.findall('\[sound:(.+?\..+?)\]', str(note))

                if not results:
                    return (True, None)
                if len(results) == 1:
                    playsound(os.path.expanduser(os.path.join(media_directory, results[0])))
                    return (True, None)
                for result in results:
                    playsound(os.path.expanduser(os.path.join(media_directory, result)))
                return (True, None)

    web_window.webview.channel = QWebChannel()
    web_window.webview.handler = CallHandler()
    web_window.webview.channel.registerObject('handler', web_window.webview.handler)
    web_window.webview.page().setWebChannel(web_window.webview.channel)

    index_html = open(index_file, "r").read().replace(
        "QUERY_PLACEHOLD", emacs_query).replace(
            "HEAD-PLACEHOLD", get_tooltip_script(script_file))
    web_window.loading_js_code = ""
    web_window.webview.setHtml(index_html, QUrl(index_file))

    web_window.web_page.loadFinished.connect(lambda: web_window.web_page.runJavaScript(f"tooltips()"))

也尝试过在 popweb.py 文件中的 hide_web_window 中尝试增加:

            web_window.webview.handler.deleteLater()
            web_window.webview.channel.deleteLater()

也没有效果。

我对 pyqt 没有接触过,完全是在盲人摸象,找了一下午,都没有头绪。

manateelazycat commented 1 year ago

我要周末看了, 这几天超级忙。

czqhurricnae commented 1 year ago

新的提交已经解决该问题。

manateelazycat commented 1 year ago

怎么解决的呀

czqhurricnae commented 1 year ago

刚开始,听取你的建议,要使用 @PostGui() 。

在 popweb-anki-review.py:


def pop_anki_review_window(popweb, module_name, index_file, x, y,
                           x_offset, y_offset, frame_x, frame_y, frame_w, frame_h,
                           width_scale, height_scale, show_window,
                           script_file, media_directory,
                           new_query_p, emacs_query):

    web_window = popweb.get_web_window(module_name)
    window_width = frame_w * width_scale
    window_height = frame_h * height_scale
    window_x, window_y = popweb.adjust_render_pos(x + x_offset, y + y_offset,
                                                  x_offset, y_offset,
                                                  window_width, window_height,
                                                  frame_x, frame_y,
                                                  frame_w, frame_h)

    class CallHandler(QObject):
        @pyqtSlot(QVariant, result=QVariant)
        def expanded_on_bridge_cmd(self, cmd: str) -> Tuple[bool, Any]:

            if cmd.startswith("rev-search "):
                tooltip_id = int(cmd.split()[1])
                query      = " ".join(cmd.split()[2:])
                if (query.isascii() and len(query) >= 3) or (not query.isascii() and len(query) >= 2):
                    notes = get_notes(query)
                    web_window.web_page.runJavaScript(f"setTooltipSearchResults({tooltip_id}, {json.dumps(notes)})")
                return (True, None)

            elif cmd.startswith("rev-tt-edit "):
                invoke("guiEditNote", note=int(cmd.split()[1]))
                return (True, None)

            elif cmd.startswith("rev-tt-playsound "):
                tooltip_id = int(cmd.split()[1])
                note       = invoke("notesInfo", notes=[tooltip_id])
                results    = re.findall('\[sound:(.+?\..+?)\]', str(note))

                if not results:
                    return (True, None)
                if len(results) == 1:
                    playsound(os.path.expanduser(os.path.join(media_directory, results[0])))
                    return (True, None)
                for result in results:
                    playsound(os.path.expanduser(os.path.join(media_directory, result)))
                return (True, None)

    web_window.webview.channel = QWebChannel()
    web_window.webview.handler = CallHandler()
    web_window.webview.channel.registerObject('handler', web_window.webview.handler)
    web_window.webview.page().setWebChannel(web_window.webview.channel)

    index_html = open(index_file, "r").read().replace(
        "QUERY_PLACEHOLD", emacs_query).replace(
            "HEAD-PLACEHOLD", get_tooltip_script(script_file))
    web_window.loading_js_code = ""
    web_window.webview.setHtml(index_html, QUrl(index_file))

    web_window.web_page.loadFinished.connect(lambda: web_window.web_page.runJavaScript(f"tooltips()"))

尝试过: 1.

@PostGui()
def pop_anki_review_window

2.

@PostGui()
@pyqtSlot(QVariant, result=QVariant)
        def expanded_on_bridge_cmd

3.

@pyqtSlot(QVariant, result=QVariant)
@PostGui()
        def expanded_on_bridge_cmd

4.

    PostGui()(web_window.webview.channel.registerObject('handler', web_window.webview.handler))
    PostGui()(web_window.webview.page().setWebChannel(web_window.webview.channel))

这几种方法后没有解决。

然后尝试将 Python 与 JS 通信的代码都写到 popweb.py 中,发现没有再出现该错误。

我研究了一下 PostGui 装饰器的代码,去找了 PyQt 的教程阅读了,了解一些 信号和槽。

我猜测 Python 与 JS 通信的注册代码:

    web_window.webview.channel = QWebChannel()
    web_window.webview.handler = CallHandler()
    web_window.webview.channel.registerObject('handler', web_window.webview.handler)
    web_window.webview.page().setWebChannel(web_window.webview.channel)

写在主线程 popweb.py 中不会产生错误。

但是因为 class CallHandler() 中很多的逻辑代码只想放在 popweb-anki-review.py 中。

所以就使用:

    @PostGui()
    def build_web_channel(self, module_path, module_name, slot_class_name, slot_method_name, slot_method_args, expose_to_js_handler_name):
        module = self.get_module(module_path)
        slot_class = getattr(module, slot_class_name)
        slot_method = getattr(slot_class, slot_method_name)
        slot_class.slot_method_name = QtCore.pyqtSlot(QtCore.QVariant, result=QtCore.QVariant)(slot_method)
        web_window = self.get_web_window(module_name)
        web_window.webview.channel = QWebChannel()
        web_window.webview.handler = slot_class(web_window, slot_method_args)
        web_window.webview.channel.registerObject(expose_to_js_handler_name, web_window.webview.handler)
        web_window.webview.page().setWebChannel(web_window.webview.channel)

利用 PostGui 中信号与槽的技术,把逻辑代码 class CallHandler()

        web_window.webview.channel = QWebChannel()
        web_window.webview.handler = slot_class(web_window, slot_method_args)
        web_window.webview.channel.registerObject(expose_to_js_handler_name, web_window.webview.handler)
        web_window.webview.page().setWebChannel(web_window.webview.channel)

这段 Python 与 JS 通信的注册代码分离。

总之:Python 与 JS 通信的注册代码在主线程运行是关键。

不知道我的理解对不对?还是只是误打误撞?

manateelazycat commented 1 year ago

补丁的截图能否换一下其他图片或者不显示图片?

这张图片本身会显示到README中, 有可能会因为图片本身让用户误解本项目。

manateelazycat commented 1 year ago

大佬,换一下gif吧

czqhurricnae commented 1 year ago

我把 gif 删除了。