class AwBrowser(QDialog): """ Customization and configuration of a web browser to run within Anki """ _parent = None _fields = [] _selectedListener = None _web = None _urlInfo = None def __init__(self, myParent): QDialog.__init__(self, myParent) self._parent = myParent self.setupUI() def setupUI(self): self.setWindowTitle('Anki :: Web Browser Addon') self.setGeometry(450, 200, 800, 450) self.setMinimumWidth (640) self.setMinimumHeight(450) mainLayout = QVBoxLayout() mainLayout.setContentsMargins(0,0,0,0) mainLayout.setSpacing(0) self.setLayout(mainLayout) topWidget = QtWidgets.QWidget(self) topWidget.setFixedHeight(50) topLayout = QtWidgets.QHBoxLayout(topWidget) topLayout.setObjectName("topLayout") lbSite = QtWidgets.QLabel(topWidget) lbSite.setObjectName("label") lbSite.setText("Website: ") topLayout.addWidget(lbSite) self._itAddress = QtWidgets.QLineEdit(topWidget) self._itAddress.setObjectName("itSite") topLayout.addWidget(self._itAddress) cbGo = QtWidgets.QCommandLinkButton(topWidget) cbGo.setObjectName("cbGo") cbGo.setFixedSize(30, 30) topLayout.addWidget(cbGo) # cbImport = QtWidgets.QCommandLinkButton(topWidget) # cbImport.setObjectName("cbImport") # cbImport.setFixedSize(30, 30) # topLayout.addWidget(cbImport) self._loadingBar = QtWidgets.QProgressBar(topWidget) self._loadingBar.setFixedWidth(100) self._loadingBar.setProperty("value", 100) self._loadingBar.setObjectName("loadingBar") topLayout.addWidget(self._loadingBar) mainLayout.addWidget(topWidget) self._web = QWebEngineView(self) self._web.contextMenuEvent = self.contextMenuEvent self._web.page().loadStarted.connect(self.onStartLoading) self._web.page().loadFinished.connect(self.onLoadFinish) self._web.page().loadProgress.connect(self.onProgress) self._web.page().urlChanged.connect(self.onPageChange) cbGo.clicked.connect(self._goToAddress) mainLayout.addWidget(self._web) if cfg.getConfig().browserAlwaysOnTop: self.setWindowFlags(Qt.WindowStaysOnTopHint) def formatTargetURL(self, website: str, query: str = ''): return website.format(urllib.parse.quote(query, encoding='utf8')) #encode('utf8', 'ignore') def open(self, website, query: str): """ Loads a given page with its replacing part with its query, and shows itself """ target = self.formatTargetURL(website, query) self._web.load(QUrl( target )) self._itAddress.setText(target) self.show() return self._web def unload(self): try: self._web.setHtml(BLANK_PAGE) except (RuntimeError) as err: pass def onClose(self): self._parent = None self._web.close() self.close() def onStartLoading(self): self._loadingBar.setProperty("value", 1) def onProgress(self, prog): self._loadingBar.setProperty("value", prog) def onLoadFinish(self, result): self._loadingBar.setProperty("value", 100) if not result: Feedback.showInfo('Error loading page') Feedback.log('Error on loading page! ', result) def _goToAddress(self): self._web.load(QUrl( self._itAddress.text() )) self._web.show() def onPageChange(self, url): self._itAddress.setText(url.toString()) def welcome(self): self._web.setHtml(WELCOME_PAGE) self.show() # ------------------------------------ Menu --------------------------------------- def _makeMenuAction(self, field, value, isLink): """ Creates correct operations for the context menu selection. Only with lambda, it would repeat only the last element """ return lambda: self._selectedListener.handleSelection(field, value, isLink) def contextMenuEvent(self, evt): """ Handles the context menu in the web view. Shows and handle options (from field list), only if in edit mode. """ if not (self._fields and self._selectedListener): return isLink = False value = None if self._web.selectedText(): isLink = False value = self._web.selectedText() else: if (self._web.page().contextMenuData().mediaType() == QWebEngineContextMenuData.MediaTypeImage and self._web.page().contextMenuData().mediaUrl()): isLink = True value = self._web.page().contextMenuData().mediaUrl() if not value: return self.createCtxMenu(value, isLink, evt) def createCtxMenu(self, value, isLink, evt): 'Creates and configures the menu itself' m = QMenu(self) sub = QMenu(Label.BROWSER_ASSIGN_TO, m) m.setTitle(Label.BROWSER_ASSIGN_TO) for index, label in self._fields.items(): act = QAction(label, m, triggered=self._makeMenuAction(index, value, isLink)) sub.addAction(act) m.addMenu(sub) action = m.exec_(self.mapToGlobal(evt.pos())) def load(self, qUrl): self._web.load(qUrl) # ----------------- getter / setter ------------------- def setFields(self, fList): self._fields = fList def setSelectionListener(self, value): self._selectedListener = value
class AwBrowser(QDialog): """ Customization and configuration of a web browser to run within Anki """ SINGLETON = None TITLE = 'Anki :: Web Browser Addon' _parent = None _fields = [] _selectionHandler = None _web = None _context = None _lastAssignedField = None infoList = [] providerList = [] def __init__(self, myParent): QDialog.__init__(self, None) self._parent = myParent self.setupUI() if myParent: def wrapClose(fn): def clozeBrowser(evt): self.close() fn(evt) return clozeBrowser myParent.closeEvent = wrapClose(myParent.closeEvent) def setupUI(self): self.setWindowTitle(AwBrowser.TITLE) self.setWindowFlags(Qt.WindowMinMaxButtonsHint | Qt.WindowCloseButtonHint) self.setGeometry(450, 200, 800, 450) self.setMinimumWidth(640) self.setMinimumHeight(450) self.setStyleSheet(Style.DARK_BG) mainLayout = QVBoxLayout() mainLayout.setContentsMargins(0, 0, 0, 0) mainLayout.setSpacing(0) self.setLayout(mainLayout) self._web = QWebEngineView(self) self._web.contextMenuEvent = self.contextMenuEvent self._web.page().loadStarted.connect(self.onStartLoading) self._web.page().loadFinished.connect(self.onLoadFinish) self._web.page().loadProgress.connect(self.onProgress) self._web.page().urlChanged.connect(self.onPageChange) # -------------------- Top / toolbar ---------------------- navtbar = QToolBar("Navigation") navtbar.setIconSize(QSize(16, 16)) mainLayout.addWidget(navtbar) backBtn = QAction( QtGui.QIcon(os.path.join(CWD, 'assets', 'arrow-back.png')), "Back", self) backBtn.setStatusTip("Back to previous page") backBtn.triggered.connect(self._web.back) navtbar.addAction(backBtn) self.forwardBtn = QAction( QtGui.QIcon(os.path.join(CWD, 'assets', 'arrow-forward.png')), "Forward", self) self.forwardBtn.setStatusTip("Next visited page") self.forwardBtn.triggered.connect(self._web.forward) navtbar.addAction(self.forwardBtn) refreshBtn = QAction( QtGui.QIcon(os.path.join(CWD, 'assets', 'reload.png')), "Reload", self) refreshBtn.setStatusTip("Reload") refreshBtn.triggered.connect(self._web.reload) navtbar.addAction(refreshBtn) self.createProvidersMenu(navtbar) self._itAddress = QtWidgets.QLineEdit(self) self._itAddress.setObjectName("itSite") self._itAddress.setStyleSheet('background-color: #F5F5F5;') self._itAddress.returnPressed.connect(self._goToAddress) navtbar.addWidget(self._itAddress) cbGo = QAction(QtGui.QIcon(os.path.join(CWD, 'assets', 'go-icon.png')), "Go", self) cbGo.setObjectName("cbGo") navtbar.addAction(cbGo) cbGo.triggered.connect(self._goToAddress) self.stopBtn = QAction( QtGui.QIcon(os.path.join(CWD, 'assets', 'stop.png')), "Stop", self) self.stopBtn.setStatusTip("Stop loading") self.stopBtn.triggered.connect(self._web.stop) navtbar.addAction(self.stopBtn) # -------------------- Center ---------------------- mainLayout.addWidget(self._web) # -------------------- Bottom bar ---------------------- bottomWidget = QtWidgets.QWidget(self) bottomWidget.setFixedHeight(30) bottomLayout = QtWidgets.QHBoxLayout(bottomWidget) bottomLayout.setObjectName("bottomLayout") bottomWidget.setStyleSheet('color: #FFF;') lbSite = QtWidgets.QLabel(bottomWidget) lbSite.setObjectName("label") lbSite.setText("Context: ") lbSite.setFixedWidth(70) lbSite.setStyleSheet('font-weight: bold;') bottomLayout.addWidget(lbSite) self.ctxWidget = QtWidgets.QLabel(bottomWidget) self.ctxWidget.width = 300 self.ctxWidget.setStyleSheet('text-align: left;') bottomLayout.addWidget(self.ctxWidget) self._loadingBar = QtWidgets.QProgressBar(bottomWidget) self._loadingBar.setFixedWidth(100) self._loadingBar.setProperty("value", 100) self._loadingBar.setObjectName("loadingBar") bottomLayout.addWidget(self._loadingBar) mainLayout.addWidget(bottomWidget) if cfg.getConfig().browserAlwaysOnTop: self.setWindowFlags(Qt.WindowStaysOnTopHint) @classmethod def singleton(cls, parent): if not cls.SINGLETON: cls.SINGLETON = AwBrowser(parent) return cls.SINGLETON def formatTargetURL(self, website: str, query: str = ''): return website.format(urllib.parse.quote(query, encoding='utf8')) @exceptionHandler def open(self, website, query: str): """ Loads a given page with its replacing part with its query, and shows itself """ self._context = query self._updateContextWidget() target = self.formatTargetURL(website, query) self._web.load(QUrl(target)) self._itAddress.setText(target) self.show() self.raise_() return self._web def unload(self): try: self._web.setHtml(BLANK_PAGE) self._itAddress.setText('about:blank') except (RuntimeError) as err: pass def onClose(self): self._parent = None self._web.close() self.close() def onStartLoading(self): self.stopBtn.setEnabled(True) self._loadingBar.setProperty("value", 1) def onProgress(self, prog): self._loadingBar.setProperty("value", prog) def onLoadFinish(self, result): self.stopBtn.setDisabled(True) self._loadingBar.setProperty("value", 100) if not result: Feedback.log('No result on loading page! ') def _goToAddress(self): q = QUrl(self._itAddress.text()) if q.scheme() == "": q.setScheme("http") self._web.load(q) self._web.show() def onPageChange(self, url): if url and url.toString().startswith('http'): self._itAddress.setText(url.toString()) self.forwardBtn.setEnabled(self._web.history().canGoForward()) def welcome(self): self._web.setHtml(WELCOME_PAGE) self._itAddress.setText('about:blank') self.show() self.raise_() def _updateContextWidget(self): self.ctxWidget.setText(self._context) # --------------------------------------------------------------------------------- def createProvidersMenu(self, parentWidget): providerBtn = QAction( QtGui.QIcon(os.path.join(CWD, 'assets', 'gear-icon.png')), "Providers", parentWidget) providerBtn.setStatusTip("Search with Provider") providerBtn.triggered.connect( lambda: self.newProviderMenu(providerBtn)) parentWidget.addAction(providerBtn) def newProviderMenu(self, parentBtn): ctx = ProviderSelectionController() ctx.showCustomMenu(parentBtn.parentWidget(), self.reOpenSameQuery) @exceptionHandler def reOpenSameQuery(self, website): self.open(website, self._context) # ------------------------------------ Menu --------------------------------------- def _makeMenuAction(self, field, value, isLink): """ Creates correct operations for the context menu selection. Only with lambda, it would repeat only the last element """ def _processMenuSelection(): self._lastAssignedField = field self._selectionHandler(field, value, isLink) return _processMenuSelection def contextMenuEvent(self, evt): """ Handles the context menu in the web view. Shows and handle options (from field list), only if in edit mode. """ if not (self._fields and self._selectionHandler): return self.createInfoMenu(evt) isLink = False value = None if self._web.selectedText(): isLink = False value = self._web.selectedText() else: if (self._web.page().contextMenuData().mediaType() == QWebEngineContextMenuData.MediaTypeImage and self._web.page().contextMenuData().mediaUrl()): isLink = True value = self._web.page().contextMenuData().mediaUrl() Feedback.log('Link: ' + value.toString()) Feedback.log('toLocal: ' + value.toLocalFile()) if not self._checkSuffix(value): return if not value: Feedback.log('No value') return self.createInfoMenu(evt) if QApplication.keyboardModifiers() == Qt.ControlModifier: if self._assignToLastField(value, isLink): return self.createCtxMenu(value, isLink, evt) def _checkSuffix(self, value): if value and not value.toString().endswith( ("jpg", "jpeg", "png", "gif")): msgLink = value.toString() if len(value.toString()) < 80: msgLink = msgLink[:50] + '...' + msgLink[50:] answ = QMessageBox.question( self, 'Anki support', """This link may not be accepted by Anki: \n\n "%s" \n Usually the suffix should be one of (jpg, jpeg, png, gif). Try it anyway? """ % msgLink, QMessageBox.Yes | QMessageBox.No) if answ != QMessageBox.Yes: return False return True def createCtxMenu(self, value, isLink, evt): 'Creates and configures the menu itself' m = QMenu(self) m.addAction(QAction('Copy', m, triggered=lambda: self._copy(value))) m.addSeparator() labelAct = QAction(Label.BROWSER_ASSIGN_TO, m) labelAct.setDisabled(True) m.addAction(labelAct) # sub = QMenu(Label.BROWSER_ASSIGN_TO, m) m.setTitle(Label.BROWSER_ASSIGN_TO) for index, label in self._fields.items(): act = QAction(label, m, triggered=self._makeMenuAction(index, value, isLink)) m.addAction(act) # m.addMenu(sub) action = m.exec_(self.mapToGlobal(evt.pos())) def createInfoMenu(self, evt): 'Creates and configures a menu with only some information' m = QMenu(self) for item in self.infoList: act = QAction(item) act.setEnabled(False) m.addAction(act) action = m.exec_(self.mapToGlobal(evt.pos())) def _assignToLastField(self, value, isLink): 'Tries to set the new value to the same field used before, if set...' if self._lastAssignedField: if self._lastAssignedField in self._fields: self._selectionHandler(self._lastAssignedField, value, isLink) return True else: self._lastAssignedField = None return False def _copy(self, value): if not value: return clip = QApplication.clipboard() clip.setText(value if isinstance(value, str) else value.toString()) def load(self, qUrl): self._web.load(qUrl) # ----------------- getter / setter ------------------- def setFields(self, fList): self._fields = fList def setSelectionHandler(self, value): self._selectionHandler = value
class MainWindow(QtWidgets.QMainWindow): """Main application window.""" def __init__(self, model_editor): """Initialize the class.""" super(MainWindow, self).__init__() self.setMinimumSize(960, 660) self._hsplitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal, self) self._model_editor = model_editor self.setCentralWidget(self._hsplitter) # tabs self._tab = QtWidgets.QTabWidget(self._hsplitter) self._tab.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) self._tab.setMinimumSize(600, 200) self._tab.sizeHint = lambda: QtCore.QSize(700, 250) self.info = QWebEngineView(self._tab) self.info.setContextMenuPolicy(QtCore.Qt.NoContextMenu) self.info_page = panels.InfoPanelPage() self.info.setPage(self.info_page) """info panel""" self.err = panels.ErrorWidget(self._tab) """error message panel""" self._tab.addTab(self.info, "Structure Info") self._tab.addTab(self.err, "Messages") # debug panel if cfg.config.DEBUG_MODE: self.debug_tab = panels.DebugPanelWidget(self._tab) self._tab.addTab(self.debug_tab, "Debug") # splitters self._vsplitter = QtWidgets.QSplitter(QtCore.Qt.Vertical, self._hsplitter) self.editor = panels.YamlEditorWidget(self._vsplitter) """main editor component""" self.tree = panels.TreeWidget(self._vsplitter) """tree data display widget""" self._vsplitter.addWidget(self.editor) self._vsplitter.addWidget(self._tab) self._hsplitter.insertWidget(0, self.tree) # Menu bar self._menu = self.menuBar() self._file_menu = MainFileMenu(self, self._model_editor) self.update_recent_files(0) self._edit_menu = MainEditMenu(self, self.editor) self._settings_menu = MainSettingsMenu(self, self._model_editor) self._menu.addMenu(self._file_menu) self._menu.addMenu(self._edit_menu) self._menu.addMenu(self._settings_menu) # status bar self._column = QtWidgets.QLabel(self) self._column.setFrameStyle(QtWidgets.QFrame.StyledPanel) self._reload_icon = QtWidgets.QLabel(self) self._reload_icon.setPixmap(icon.get_pixmap("refresh", 16)) self._reload_icon.setVisible(False) self._reload_icon_timer = QtCore.QTimer(self) self._reload_icon_timer.timeout.connect(lambda: self._reload_icon.setVisible(False)) cfg.config.observers.append(self) self._status = self.statusBar() self._status.addPermanentWidget(self._reload_icon) self._status.addPermanentWidget(self._column) self.setStatusBar(self._status) self._status.showMessage("Ready", 5000) # signals self.err.itemSelected.connect(self._item_selected) self.tree.itemSelected.connect(self._item_selected) self.editor.nodeChanged.connect(self._reload_node) self.editor.cursorChanged.connect(self._cursor_changed) self.editor.structureChanged.connect(self._structure_changed) self.editor.errorMarginClicked.connect(self._error_margin_clicked) self.editor.elementChanged.connect(lambda new, old: self._update_info(new)) self.editor.nodeSelected.connect(self._on_node_selected) # initialize components self._update_info(None) self.config_changed() # set focus self.editor.setFocus() def keyPressEvent(self, event): if event.matches(QKeySequence.Copy) and self.info.selectedText() != "": QtWidgets.QApplication.clipboard().setText(self.info.selectedText()) else: super().keyReleaseEvent(event) def reload(self): """reload panels after structure changes""" self._reload_icon.setVisible(True) self._reload_icon.update() self.editor.setUpdatesEnabled(False) cfg.update() self.editor.setUpdatesEnabled(True) self.editor.reload() self.tree.reload() self.err.reload() line, index = self.editor.getCursorPosition() self._reload_node(line+1, index+1) self._reload_icon_timer.start(700) def show_status_message(self, message, duration=5000): """Show a message in status bar for the given duration (in ms).""" self._status.showMessage(message, duration) def update_recent_files(self, from_row=1): """Update recently opened files.""" self._file_menu.update_recent_files(from_row) def _item_selected(self, start_line, start_column, end_line, end_column): """Handle when an item is selected from tree or error tab. :param int start_line: line where the selection starts :param int start_column: column where the selection starts :param int end_line: line where the selection ends :param int end_column: column where the selection ends """ self.editor.setFocus() # remove empty line and whitespaces at the end of selection if end_line > start_line and end_line > 1: last_line_text = self.editor.text(end_line-1) if end_column > len(last_line_text): end_column = len(last_line_text) + 1 last_line_text_selected = last_line_text[:end_column-1] if LineAnalyzer.is_empty(last_line_text_selected): end_line -= 1 end_line_text = self.editor.text(end_line-1) end_column = len(end_line_text) # select in reversed order - move cursor to the beginning of selection self.editor.mark_selected(end_line, end_column, start_line, start_column) def _reload_node(self, line, index): """reload info after changing node selection""" node = cfg.get_data_node(Position(line, index)) self.editor.set_new_node(node) cursor_type = self.editor.cursor_type_position self._update_info(cursor_type) if cfg.config.DEBUG_MODE: self.debug_tab.show_data_node(node) def _cursor_changed(self, line, column): """Editor node change signal""" self._column.setText("Line: {:5d} Pos: {:3d}".format(line, column)) def _structure_changed(self, line, column): """Editor structure change signal""" if cfg.update_yaml_file(self.editor.text()): self.reload() else: self._reload_node(line, column) def _error_margin_clicked(self, line): """Click error icon in margin""" self._tab.setCurrentIndex(self._tab.indexOf(self.err)) self.err.select_error(line) def _update_info(self, cursor_type): """Update the info panel.""" if self.editor.pred_parent is not None: self.info_page.update_from_node(self.editor.pred_parent, CursorType.parent.value) return if self.editor.curr_node is not None: self.info_page.update_from_node(self.editor.curr_node, cursor_type) return # show root input type info by default self.info_page.update_from_data({'record_id': cfg.root_input_type['id']}, True) return def _on_node_selected(self, line, column): """Handles nodeSelected event from editor.""" node = cfg.get_data_node(Position(line, column)) self.tree.select_data_node(node) def closeEvent(self, event): """Performs actions before app is closed.""" # prompt user to save changes (if any) if not self._model_editor.save_old_file(): return event.ignore() super(MainWindow, self).closeEvent(event) def config_changed(self): """Handle changes of config.""" self.editor.set_line_endings(cfg.config.line_endings)
class MainWindow(BaseWindow): def __init__(self): super().__init__() self.qObject = QObject() self.initConsole() self.initUI() self.show() def initConsole(self): self.consoleWindow = ButtomWindow() def initUI(self): self.resize(int(Utils.getWindowWidth() * 0.8), int(Utils.getWindowHeight() * 0.8)) self.initMenuBar() self.initLayout() super().initWindow() def initMenuBar(self): menuBar = self.menuBar() fileMenu = menuBar.addMenu('File') disConnectAction = QAction(IconTool.buildQIcon('disconnect.png'), '&Disconnect', self) disConnectAction.setShortcut('Ctrl+D') disConnectAction.setShortcutContext(Qt.ApplicationShortcut) disConnectAction.triggered.connect(self.restartProgram) settingAction = QAction(IconTool.buildQIcon('setting.png'), '&Setting...', self) settingAction.setShortcut('Ctrl+Shift+S') settingAction.setShortcutContext(Qt.ApplicationShortcut) settingAction.triggered.connect(lambda: STCLogger().d('setting')) clearCacheAction = QAction(IconTool.buildQIcon('clearCache.png'), '&ClearCache', self) clearCacheAction.setShortcut('Ctrl+Alt+C') clearCacheAction.setShortcutContext(Qt.ApplicationShortcut) clearCacheAction.triggered.connect(self.clearCache) clearSearchHistoryAction = QAction( IconTool.buildQIcon('clearCache.png'), '&ClearSearchHistory', self) clearSearchHistoryAction.triggered.connect(self.clearSearchHistory) settingLogPathAction = QAction(IconTool.buildQIcon('path.png'), '&LogPath', self) settingLogPathAction.triggered.connect(self.setLogPath) fileMenu.addAction(disConnectAction) fileMenu.addAction(clearCacheAction) fileMenu.addAction(settingLogPathAction) fileMenu.addAction(clearSearchHistoryAction) # fileMenu.addAction(settingAction) # fileMenu.addAction(showLogAction) # settingAction.triggered.connect(self.openSettingWindow) editMenu = menuBar.addMenu('Edit') findAction = QAction(IconTool.buildQIcon('find.png'), '&Find', self) findAction.setShortcut('Ctrl+F') findAction.triggered.connect(self.findActionClick) editMenu.addAction(findAction) focusItemAction = QAction(IconTool.buildQIcon('focus.png'), '&Focus Item', self) focusItemAction.triggered.connect(self.focusChooseItem) editMenu.addAction(focusItemAction) self.chooseItemType = '' self.chooseItemId = '' settingMenu = menuBar.addMenu('Setting') autoLoginAction = QAction(IconTool.buildQIcon('setting.png'), '&Auto Login', self) autoLoginAction.setShortcutContext(Qt.ApplicationShortcut) autoLoginAction.triggered.connect(self.setAutoState) settingMenu.addAction(autoLoginAction) helpMenu = menuBar.addMenu('Help') aboutAction = QAction(IconTool.buildQIcon('about.png'), '&About', self) aboutAction.triggered.connect(self.openAboutWindow) helpMenu.addAction(aboutAction) def closeEvent(self, e): self.settings.setValue('searchHistory', self.searchHistory) e.accept() def openAboutWindow(self): print('open about') self.aboutWindow = AboutWindow() self.aboutWindow.show() def setAutoState(self): reply = QMessageBox.question(self, "提示", "是否取消自动登录功能", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: Utils.setAutoLoginState(False) def restartProgram(self): from XulDebugTool.ui.ConnectWindow import ConnectWindow # 不应该在这里导入,但是放在前面会有问题 try: hasAbout = object.__getattribute__(self, "aboutWindow") except: hasAbout = None if hasAbout is not None: self.aboutWindow.close() print("新建连接页面") self.con = ConnectWindow() self.close() # def openSettingWindow(self): # self.tableInfoModel = SettingWindow() # self.tableInfoModel.show() def initLayout(self): # ----------------------------left layout---------------------------- # self.treeModel = QStandardItemModel() self.pageItem = QStandardItem(ROOT_ITEM_PAGE) self.pageItem.type = ITEM_TYPE_PAGE_ROOT self.buildPageItem() self.userobjectItem = QStandardItem(ROOT_ITEM_USER_OBJECT) self.userobjectItem.type = ITEM_TYPE_USER_OBJECT_ROOT self.buildUserObjectItem() self.providerRequestItem = QStandardItem(ROOT_ITEM_PROVIDER_REQUESTS) self.providerRequestItem.type = ITEM_TYPE_PROVIDER_REQUESTS_ROOT self.pluginItem = QStandardItem(ROOT_ITEM_PLUGIN) self.pluginItem.type = ITEM_TYPE_PLUGIN_ROOT self.treeModel.appendColumn([ self.pageItem, self.userobjectItem, self.providerRequestItem, self.pluginItem ]) self.treeModel.setHeaderData(0, Qt.Horizontal, 'Model') self.treeView = QTreeView() self.treeView.setModel(self.treeModel) self.treeView.setEditTriggers(QAbstractItemView.NoEditTriggers) self.treeView.setContextMenuPolicy(Qt.CustomContextMenu) self.treeView.customContextMenuRequested.connect(self.openContextMenu) self.treeView.doubleClicked.connect(self.onTreeItemDoubleClicked) self.treeView.clicked.connect(self.getDebugData) leftContainer = QWidget() layout = QVBoxLayout() layout.setContentsMargins(0, 0, 6, 0) # left, top, right, bottom layout.addWidget(self.treeView) leftContainer.setLayout(layout) # ----------------------------middle layout---------------------------- # middleContainer = QWidget() # search shall start not before the user completed typing # filter_delay = DelayedExecutionTimer(self) # new_column.search_bar.textEdited[str].connect(filter_delay.trigger) # filter_delay.triggered[str].connect(self.search) self.tabBar = QTabBar() self.tabBar.setUsesScrollButtons(False) self.tabBar.setDrawBase(False) # self.tabBar.addTab('tab1') # self.tabBar.addTab('tab2') self.pathBar = QWidget() layout = QHBoxLayout() layout.setAlignment(Qt.AlignLeft) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(1) self.pathBar.setLayout(layout) self.searchHolder = QWidget() layout = QHBoxLayout() layout.addWidget(self.tabBar) layout.addWidget(self.pathBar) layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Expanding)) self.searchHolder.setLayout(layout) self.searchHolder.layout().setContentsMargins(6, 6, 6, 0) self.tabContentWidget = QWidget() self.browser = QWebEngineView() self.browser.setZoomFactor(1.3) self.channel = QWebChannel() self.webObject = WebShareObject() self.channel.registerObject('bridge', self.webObject) self.browser.page().setWebChannel(self.channel) self.webObject.jsCallback.connect(lambda value: self.addUpdate(value)) qwebchannel_js = QFile(':/qtwebchannel/qwebchannel.js') if not qwebchannel_js.open(QIODevice.ReadOnly): raise SystemExit('Failed to load qwebchannel.js with error: %s' % qwebchannel_js.errorString()) qwebchannel_js = bytes(qwebchannel_js.readAll()).decode('utf-8') script = QWebEngineScript() script.setSourceCode(qwebchannel_js) script.setInjectionPoint(QWebEngineScript.DocumentCreation) script.setName('qtwebchannel.js') script.setWorldId(QWebEngineScript.MainWorld) script.setRunsOnSubFrames(True) self.browser.page().scripts().insert(script) Utils.scriptCreator(os.path.join('..', 'resources', 'js', 'event.js'), 'event.js', self.browser.page()) self.browser.page().setWebChannel(self.channel) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.initQCheckBoxUI()) layout.addWidget(self.initSearchView()) layout.addWidget(self.browser) self.tabContentWidget.setLayout(layout) self.searchWidget.hide() middleContainer.stackedWidget = QStackedWidget() self.url = XulDebugServerHelper.HOST + 'list-pages' self.showXulDebugData(self.url) middleContainer.stackedWidget.addWidget(self.tabContentWidget) middleContainer.stackedWidget.addWidget(QLabel('tab2 content')) self.tabBar.currentChanged.connect( lambda: middleContainer.stackedWidget.setCurrentIndex( self.tabBar.currentIndex())) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.searchHolder) layout.addWidget(middleContainer.stackedWidget) middleContainer.setLayout(layout) # ----------------------------right layout---------------------------- # self.rightSiderClickInfo = 'Property' self.rightSiderTabWidget = QTabWidget() self.rightSiderTabBar = QTabBar() self.rightSiderTabWidget.setTabBar(self.rightSiderTabBar) self.rightSiderTabWidget.setTabPosition(QTabWidget.East) self.favoriteTreeView = FavoriteTreeView(self) # self.propertyEditor = PropertyEditor(['Key', 'Value']) self.inputWidget = UpdateProperty() self.rightSiderTabWidget.addTab(self.inputWidget, IconTool.buildQIcon('property.png'), 'Property') self.rightSiderTabWidget.setStyleSheet( ('QTab::tab{height:60px;width:32px;color:black;padding:0px}' 'QTabBar::tab:selected{background:lightgray}')) # self.rightSiderTabWidget.addTab(self.propertyEditor,IconTool.buildQIcon('property.png'),'property') self.rightSiderTabWidget.addTab(self.favoriteTreeView, IconTool.buildQIcon('favorites.png'), 'Favorites') self.rightSiderTabBar.tabBarClicked.connect(self.rightSiderClick) # ----------------------------entire layout---------------------------- # self.contentSplitter = QSplitter(Qt.Horizontal) self.contentSplitter.setHandleWidth(0) # thing to grab the splitter self.contentSplitter.addWidget(leftContainer) self.contentSplitter.addWidget(middleContainer) self.contentSplitter.addWidget(self.rightSiderTabWidget) self.contentSplitter.setStretchFactor(0, 0) self.contentSplitter.setStretchFactor(1, 6) self.contentSplitter.setStretchFactor(2, 6) self.mainSplitter = QSplitter(Qt.Vertical) self.mainSplitter.setHandleWidth(0) self.mainSplitter.addWidget(self.contentSplitter) self.mainSplitter.addWidget(self.consoleWindow) self.mainSplitter.setStretchFactor(1, 0) self.mainSplitter.setStretchFactor(2, 1) self.setCentralWidget(self.mainSplitter) # 默认隐藏掉复选框 self.groupBox.setHidden(True) def addUpdate(self, value=None): self.inputWidget.initData(self.pageId, value) self.inputWidget.updateItemUI() dict = json.loads(value) if dict['action'] == "click": self.chooseItemId = dict['Id'] self.chooseItemType = Utils.findNodeById(dict['Id'], dict['xml']).tag elif dict['action'] == "load": self.browser.load(QUrl(dict['url'])) else: pass def focusChooseItem(self): if self.chooseItemType in ('area', 'item'): XulDebugServerHelper.focusChooseItemUrl(self.chooseItemId) def initQCheckBoxUI(self): self.groupBox = QGroupBox() self.skipPropCheckBox = QCheckBox(SKIP_PROP, self) self.skipPropCheckBox.setChecked(False) self.skipPropCheckBox.stateChanged.connect( lambda: self.clickCheckBox(self.skipPropCheckBox, SKIP_PROP)) self.withChildrenCheckBox = QCheckBox(WITH_CHILDREN, self) self.withChildrenCheckBox.setChecked(False) self.withChildrenCheckBox.stateChanged.connect( lambda: self.clickCheckBox(self.withChildrenCheckBox, WITH_CHILDREN )) self.withBindingDataCheckBox = QCheckBox(WITH_BINDING_DATA, self) self.withBindingDataCheckBox.setChecked(False) self.withBindingDataCheckBox.stateChanged.connect( lambda: self.clickCheckBox(self.withBindingDataCheckBox, WITH_BINDING_DATA)) self.withPositionCheckBox = QCheckBox(WITH_POSITION, self) self.withPositionCheckBox.setChecked(False) self.withPositionCheckBox.stateChanged.connect( lambda: self.clickCheckBox(self.withPositionCheckBox, WITH_POSITION )) self.withSelectorCheckBox = QCheckBox(WITH_SELECTOR, self) self.withSelectorCheckBox.setChecked(False) self.withSelectorCheckBox.stateChanged.connect( lambda: self.clickCheckBox(self.withSelectorCheckBox, WITH_SELECTOR )) checkGrouplayout = QHBoxLayout() checkGrouplayout.addWidget(self.skipPropCheckBox) checkGrouplayout.addWidget(self.withChildrenCheckBox) checkGrouplayout.addWidget(self.withBindingDataCheckBox) checkGrouplayout.addWidget(self.withPositionCheckBox) checkGrouplayout.addWidget(self.withSelectorCheckBox) self.groupBox.setLayout(checkGrouplayout) return self.groupBox def initSearchView(self): self.settings = QSettings('XulDebugTool') self.searchHistory = self.settings.value('searchHistory', []) self.searchWidget = QWidget() self.searchWidget.setStyleSheet( ".QWidget{border:1px solid rgb(220, 220, 220)}") searchPageLayout = QHBoxLayout() self.searchLineEdit = QLineEdit() self.searchIcon = QAction(self) self.searchIcon.setIcon(IconTool.buildQIcon('find_h.png')) self.searchMenu = QMenu(self) for text in self.searchHistory: self.action = QAction( text, self, triggered=lambda: self.searchLineEdit.setText(text)) self.searchMenu.addAction(self.action) self.searchIcon.setMenu(self.searchMenu) self.searchDelIcon = QAction(self) self.searchDelIcon.setIcon(IconTool.buildQIcon('del.png')) self.searchLineEdit.addAction(self.searchIcon, QLineEdit.LeadingPosition) self.searchLineEdit.addAction(self.searchDelIcon, QLineEdit.TrailingPosition) self.searchDelIcon.setVisible(False) self.searchLineEdit.setStyleSheet( "border:2px groove gray;border-radius:10px;padding:2px 4px") searchPageLayout.addWidget(self.searchLineEdit) self.searchLineEdit.textChanged.connect(self.searchPage) self.searchLineEdit.editingFinished.connect(self.saveSearchHistory) self.searchDelIcon.triggered.connect(self.searchDelClick) self.previousBtn = QPushButton() self.previousBtn.setStyleSheet("background:transparent;") self.previousBtn.setIcon(IconTool.buildQIcon('up.png')) self.previousBtn.setFixedSize(15, 20) searchPageLayout.addWidget(self.previousBtn) self.previousBtn.clicked.connect( lambda: self.previousBtnClick(self.searchLineEdit.text())) self.nextBtn = QPushButton() self.nextBtn.setIcon(IconTool.buildQIcon('down.png')) self.nextBtn.setStyleSheet("background:transparent;") self.nextBtn.setFixedSize(15, 20) self.nextBtn.clicked.connect( lambda: self.nextBtnClick(self.searchLineEdit.text())) searchPageLayout.addWidget(self.nextBtn) self.matchCase = QCheckBox("Match Case") self.matchCase.setChecked(False) self.matchCase.stateChanged.connect(self.matchCaseChange) searchPageLayout.addWidget(self.matchCase) self.matchTips = QLabel() self.matchTips.setFixedWidth(100) self.searchClose = QPushButton("×") self.searchClose.setFixedWidth(10) self.searchClose.setStyleSheet("background:transparent;") self.searchClose.clicked.connect(self.searchCloseClick) searchPageLayout.addWidget(self.matchTips) searchPageLayout.addWidget(self.searchClose) self.searchWidget.setLayout(searchPageLayout) return self.searchWidget def clickCheckBox(self, checkBox, name): if checkBox.isChecked(): STCLogger().i('select ' + name) self.selectCheckBoxInfo(name) else: STCLogger().i('cancel ' + name) self.cancelCheckBoxInfo(name) def selectCheckBoxInfo(self, str): if None != self.url: checkedStr = str + '=' + 'true' unCheckedStr = str + '=' + 'false' if self.url.find('?') == -1: self.url += '?' self.url += checkedStr else: if self.url.find(str) == -1: self.url += '&' self.url += checkedStr elif self.url.find(unCheckedStr) != -1: self.url = self.url.replace(unCheckedStr, checkedStr) self.showXulDebugData(self.url) def cancelCheckBoxInfo(self, str): if None != self.url: checkedStr = str + '=' + 'true' if self.url.find(checkedStr) >= -1: split = self.url.split(checkedStr) self.url = ''.join(split) self.url = self.url.replace('&&', '&') self.url = self.url.replace('?&', '?') if self.url.endswith('?'): self.url = self.url[:-1] if self.url.endswith('&'): self.url = self.url[:-1] self.showXulDebugData(self.url) def rightSiderClick(self, index): # 两次单击同一个tabBar时显示隐藏内容区域 if self.rightSiderTabBar.tabText(index) == self.rightSiderClickInfo: if self.rightSiderTabWidget.width() == Utils.getItemHeight(): self.rightSiderTabWidget.setMaximumWidth( Utils.getWindowWidth()) self.rightSiderTabWidget.setMinimumWidth(Utils.getItemHeight()) else: self.rightSiderTabWidget.setFixedWidth(Utils.getItemHeight()) else: if self.rightSiderTabWidget.width() == Utils.getItemHeight(): self.rightSiderTabWidget.setMaximumWidth( Utils.getWindowWidth()) self.rightSiderTabWidget.setMinimumWidth(Utils.getItemHeight()) self.rightSiderClickInfo = self.rightSiderTabBar.tabText(index) def clearCache(self): r = XulDebugServerHelper.clearAllCaches() if r.status == 200: self.statusBar().showMessage('cache cleanup success') else: self.statusBar().showMessage('cache cleanup failed') def setLogPath(self): file_path = QFileDialog.getSaveFileName(self, 'save file', ConfigHelper.LOGCATPATH, "Txt files(*.txt)") if len(file_path[0]) > 0: ConfigurationDB.saveConfiguration(ConfigHelper.KEY_LOGCATPATH, file_path[0]) @pyqtSlot(QPoint) def openContextMenu(self, point): index = self.treeView.indexAt(point) if not index.isValid(): return item = self.treeModel.itemFromIndex(index) menu = QMenu() if item.type == ITEM_TYPE_PROVIDER: queryAction = QAction( IconTool.buildQIcon('data.png'), '&Query Data...', self, triggered=lambda: self.showQueryDialog(item.data)) queryAction.setShortcut('Alt+Q') menu.addAction(queryAction) copyAction = QAction( IconTool.buildQIcon('copy.png'), '&Copy', self, triggered=lambda: pyperclip.copy('%s' % index.data())) copyAction.setShortcut(QKeySequence.Copy) menu.addAction(copyAction) menu.exec_(self.treeView.viewport().mapToGlobal(point)) @pyqtSlot(QModelIndex) def getDebugData(self, index): item = self.treeModel.itemFromIndex(index) if item.type == ITEM_TYPE_PAGE_ROOT: # 树第一层,page节点 self.buildPageItem() self.url = XulDebugServerHelper.HOST + 'list-pages' self.showXulDebugData(self.url) elif item.type == ITEM_TYPE_USER_OBJECT_ROOT: # 树第一层,userObject节点 self.buildUserObjectItem() self.url = XulDebugServerHelper.HOST + 'list-user-objects' self.showXulDebugData(self.url) elif item.type == ITEM_TYPE_PROVIDER_REQUESTS_ROOT: # 树第一层,Provider Request节点 self.url = XulDebugServerHelper.HOST + 'list-provider-requests' self.showXulDebugData(self.url) elif item.type == ITEM_TYPE_PLUGIN_ROOT: # 树第一层,plugin节点 pass elif item.type == ITEM_TYPE_PAGE: # 树第二层,page下的子节点 pageId = item.id self.url = XulDebugServerHelper.HOST + 'get-layout/' + pageId self.showXulDebugData(self.url) elif item.type == ITEM_TYPE_USER_OBJECT: # 树第二层,userObject下的子节点 objectId = item.id self.url = XulDebugServerHelper.HOST + 'get-user-object/' + objectId self.showXulDebugData(self.url) elif item.type == ITEM_TYPE_PROVIDER: # 树第三层,userObject下的DataService下的子节点 pass self.groupBox.setHidden(item.type != ITEM_TYPE_PAGE) # self.fillPropertyEditor(item.data) @pyqtSlot(QModelIndex) def onTreeItemDoubleClicked(self, index): item = self.treeModel.itemFromIndex(index) if item.type == ITEM_TYPE_PROVIDER: self.showQueryDialog(item.data) def buildPageItem(self): self.pageItem.removeRows(0, self.pageItem.rowCount()) r = XulDebugServerHelper.listPages() if r: pagesNodes = Utils.xml2json(r.data, 'pages') if pagesNodes == '': return # 如果只有一个page,转化出来的json不是数据.分开处理 if isinstance(pagesNodes['page'], list): for i, page in enumerate(pagesNodes['page']): # 把page解析了以后放page节点下 row = QStandardItem(page['@pageId']) row.id = self.pageId = page['@id'] row.data = page row.type = ITEM_TYPE_PAGE self.pageItem.appendRow(row) else: page = pagesNodes['page'] row = QStandardItem(page['@pageId']) row.id = self.pageId = page['@id'] row.data = page row.type = ITEM_TYPE_PAGE self.pageItem.appendRow(row) if self.pageItem.rowCount() > 0: self.pageItem.setText( '%s(%s)' % (ROOT_ITEM_PAGE, self.pageItem.rowCount())) def buildUserObjectItem(self): self.userobjectItem.removeRows(0, self.userobjectItem.rowCount()) r = XulDebugServerHelper.listUserObject() if r: userObjectNodes = Utils.xml2json(r.data, 'objects') # 如果只有一个userObject,转化出来的json不是数据.分开处理 if userObjectNodes and isinstance(userObjectNodes['object'], list): for i, o in enumerate(userObjectNodes['object']): # 把userObject加到User-Object节点下 row = QStandardItem(o['@name']) row.id = o['@id'] row.data = o row.type = ITEM_TYPE_USER_OBJECT self.userobjectItem.appendRow(row) # 如果是DataServcie, 填充所有的Provider到该节点下 if o['@name'] == CHILD_ITEM_DATA_SERVICE: r = XulDebugServerHelper.getUserObject(o['@id']) if r: dataServiceNodes = Utils.xml2json(r.data, 'object') if isinstance( dataServiceNodes['object']['provider'], list): for j, provider in enumerate( dataServiceNodes['object'] ['provider']): dsRow = QStandardItem( provider['ds']['@providerClass']) dsRow.id = provider['@name'] dsRow.data = provider dsRow.type = ITEM_TYPE_PROVIDER row.appendRow(dsRow) else: provider = dataServiceNodes['object'][ 'provider'] dsRow = QStandardItem( provider['ds']['@providerClass']) dsRow.id = provider['@name'] dsRow.data = provider dsRow.type = ITEM_TYPE_PROVIDER row.appendRow(dsRow) # 对Provider按升序排序 row.sortChildren(0) if row.rowCount() > 0: row.setText('%s(%s)' % (row.text(), row.rowCount())) else: # 没有只有一个userObject的情况, 暂不处理 pass if self.userobjectItem.rowCount() > 0: self.userobjectItem.setText( '%s(%s)' % (ROOT_ITEM_USER_OBJECT, self.userobjectItem.rowCount())) def showXulDebugData(self, url): STCLogger().i('request url:' + url) self.browser.load(QUrl(url)) self.statusBar().showMessage(url) def convertProperty(self, k, v): """递归的将多层属性字典转成单层的.""" if isinstance(v, dict): for subk, subv in v.items(): self.convertProperty(subk, subv) else: setattr(self.qObject, k, v) def showQueryDialog(self, data): STCLogger().i('show query dialog: ', data) self.dialog = DataQueryDialog(data) self.dialog.finishSignal.connect(self.onGetQueryUrl) self.dialog.show() def onGetQueryUrl(self, url): STCLogger().i('request url:' + url) self.favoriteTreeView.updateTree() self.browser.load(QUrl(url)) self.statusBar().showMessage(url) def findActionClick(self): self.searchWidget.show() self.searchLineEdit.setFocus() self.searchLineEdit.setText(self.browser.selectedText()) def searchPage(self, text): if not text.strip(): self.searchDelIcon.setVisible(False) else: self.searchDelIcon.setVisible(True) check = self.matchCase.isChecked() if check: self.browser.findText(text, QWebEnginePage.FindFlags(2), lambda result: self.changeMatchTip(result)) else: self.browser.findText(text, QWebEnginePage.FindFlags(0), lambda result: self.changeMatchTip(result)) def saveSearchHistory(self): text = self.searchLineEdit.text() if text != '' and text not in self.searchHistory: self.action = QAction( text, self, triggered=lambda: self.searchLineEdit.setText(text)) self.searchMenu.addAction(self.action) self.searchHistory.append(text) def clearSearchHistory(self): self.searchHistory = [] def previousBtnClick(self, text): check = self.matchCase.isChecked() if check: self.browser.findText( text, QWebEnginePage.FindFlags(1) | QWebEnginePage.FindFlags(2), lambda result: self.changeMatchTip(result)) else: self.browser.findText(text, QWebEnginePage.FindFlags(1), lambda result: self.changeMatchTip(result)) def nextBtnClick(self, text): check = self.matchCase.isChecked() if check: self.browser.findText(text, QWebEnginePage.FindFlags(2), lambda result: self.changeMatchTip(result)) else: self.browser.findText(text, QWebEnginePage.FindFlags(0), lambda result: self.changeMatchTip(result)) def matchCaseChange(self): self.browser.findText("") self.searchPage(self.searchLineEdit.text()) def changeMatchTip(self, result): if result: self.matchTips.setText("Find matches") else: self.matchTips.setText("No matches") def searchDelClick(self): self.searchLineEdit.setText("") self.browser.findText("") self.matchTips.setText("") def searchCloseClick(self): self.searchLineEdit.setText("") self.browser.findText("") self.matchTips.setText("") self.searchWidget.hide()