From df27d40587cefc25d3c5f34179c38bc890012dcd Mon Sep 17 00:00:00 2001 From: copper Date: Thu, 5 May 2022 20:08:52 +0800 Subject: [PATCH] fix plugin bugs --- 3rd/about/__init__.py | 1 + 3rd/about/main.py | 68 +++++++++++++++++++++++++++++++ plugins/about/__init__.py | 1 + plugins/about/main.py | 14 ++++++- rscder/gui/actions.py | 15 +++++++ rscder/gui/license.py | 2 +- rscder/gui/mainwindow.py | 13 ++++++ rscder/gui/plugins.py | 79 ++++++++++++++++++++++++------------- rscder/mul/mulstart.py | 8 ++-- rscder/plugins/basic.py | 14 +++++-- rscder/plugins/loader.py | 50 +++++++++++++++++++---- rscder/res.qrc | 44 ++++++++++----------- rscder/utils/generate_rc.py | 16 ++++++++ rscder/utils/project.py | 2 +- rscder/utils/setting.py | 6 ++- main.py => run.py | 0 16 files changed, 262 insertions(+), 71 deletions(-) create mode 100644 3rd/about/__init__.py create mode 100644 3rd/about/main.py create mode 100644 plugins/about/__init__.py create mode 100644 rscder/utils/generate_rc.py rename main.py => run.py (100%) diff --git a/3rd/about/__init__.py b/3rd/about/__init__.py new file mode 100644 index 0000000..77caec2 --- /dev/null +++ b/3rd/about/__init__.py @@ -0,0 +1 @@ +from about.main import * \ No newline at end of file diff --git a/3rd/about/main.py b/3rd/about/main.py new file mode 100644 index 0000000..a6e0e4e --- /dev/null +++ b/3rd/about/main.py @@ -0,0 +1,68 @@ +from rscder.plugins.basic import BasicPlugin + +from PyQt5.QtWidgets import QDialog, QAction, QApplication, QLabel, QTextEdit, QVBoxLayout +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon + +class AboutDialog(QDialog): + def __init__(self, parent=None): + super(AboutDialog, self).__init__(parent) + self.setWindowTitle("About") + self.setFixedSize(800, 400) + + self.label = QLabel("

"+ QApplication.applicationName() + "

") + self.label.setAlignment(Qt.AlignCenter) + self.label.setStyleSheet("font-size: 20px;") + + self.label2 = QLabel("

Version: " + QApplication.applicationVersion() + "

") + self.label2.setAlignment(Qt.AlignCenter) + self.label2.setStyleSheet("font-size: 15px;") + + self.label3 = QLabel("

" + QApplication.organizationName() + "

") + self.label3.setAlignment(Qt.AlignCenter) + self.label3.setStyleSheet("font-size: 15px;") + + self.label4 = QLabel("

Copyright (c) 2020

") + self.label4.setAlignment(Qt.AlignCenter) + self.label4.setStyleSheet("font-size: 10px;") + + self.text = QTextEdit() + self.text.setReadOnly(True) + self.text.setText(''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + ''') + + self.layout = QVBoxLayout() + self.layout.addWidget(self.label) + self.layout.addWidget(self.label2) + self.layout.addWidget(self.label3) + self.layout.addWidget(self.label4) + self.layout.addWidget(self.text) + self.setLayout(self.layout) + + +class AboutPlugin(BasicPlugin): + + @staticmethod + def info(): + return { + 'name': '关于', + 'author': 'RSCDER', + 'version': '1.0.0', + 'description': '关于' + } + + def set_action(self): + menu = self.ctx['help_menu'] + action = QAction('&关于', self.ctx['menu_bar']) + action.triggered.connect(self.on_about) + + menu.addAction(action) + + def on_about(self): + print('on_about') + dialog = AboutDialog(self.ctx['mainwindow']) + dialog.show() \ No newline at end of file diff --git a/plugins/about/__init__.py b/plugins/about/__init__.py new file mode 100644 index 0000000..77caec2 --- /dev/null +++ b/plugins/about/__init__.py @@ -0,0 +1 @@ +from about.main import * \ No newline at end of file diff --git a/plugins/about/main.py b/plugins/about/main.py index c84eb68..a6e0e4e 100644 --- a/plugins/about/main.py +++ b/plugins/about/main.py @@ -46,13 +46,23 @@ class AboutDialog(QDialog): class AboutPlugin(BasicPlugin): + @staticmethod + def info(): + return { + 'name': '关于', + 'author': 'RSCDER', + 'version': '1.0.0', + 'description': '关于' + } + def set_action(self): menu = self.ctx['help_menu'] - action = QAction('&关于', self.ctx['menubar']) + action = QAction('&关于', self.ctx['menu_bar']) action.triggered.connect(self.on_about) menu.addAction(action) def on_about(self): - dialog = AboutDialog(self.ctx['main_window']) + print('on_about') + dialog = AboutDialog(self.ctx['mainwindow']) dialog.show() \ No newline at end of file diff --git a/rscder/gui/actions.py b/rscder/gui/actions.py index c698d0e..485e102 100644 --- a/rscder/gui/actions.py +++ b/rscder/gui/actions.py @@ -47,6 +47,21 @@ class ActionManager(QtCore.QObject): self.help_menu = menubar.addMenu('&帮助') + @property + def menus(self): + return { + 'file_menu': self.file_menu, + 'basic_menu': self.basic_menu, + 'change_detection_menu': self.change_detection_menu, + 'special_chagne_detec_menu': self.special_chagne_detec_menu, + 'seg_chagne_detec_menu': self.seg_chagne_detec_menu, + 'postop_menu': self.postop_menu, + 'view_menu': self.view_menu, + 'plugin_menu': self.plugin_menu, + 'help_menu': self.help_menu, + 'menu_bar': self.menubar + } + def set_toolbar(self, toolbar): self.toolbar = toolbar self.toolbar.setIconSize(QtCore.QSize(24, 24)) diff --git a/rscder/gui/license.py b/rscder/gui/license.py index 058fc73..0d2033a 100644 --- a/rscder/gui/license.py +++ b/rscder/gui/license.py @@ -11,7 +11,7 @@ class License(QtWidgets.QDialog): def __init__(self, parent = None, flags = QtCore.Qt.WindowFlags() ) -> None: super().__init__(parent, flags) self.setWindowTitle("License") - self.setWindowIcon(QIcon(os.path.join("gui", "icons", "license.png"))) + self.setWindowIcon(QIcon(':/icons/license.png')) self.setWindowFlags(QtCore.Qt.WindowCloseButtonHint) self.setFixedSize(600, 400) diff --git a/rscder/gui/mainwindow.py b/rscder/gui/mainwindow.py index 358e33b..9c37776 100644 --- a/rscder/gui/mainwindow.py +++ b/rscder/gui/mainwindow.py @@ -9,6 +9,7 @@ from rscder.gui.layertree import LayerTree from rscder.gui.mapcanvas import DoubleCanvas from rscder.gui.messagebox import MessageBox from rscder.gui.result import ResultTable +from rscder.plugins.loader import PluginLoader from rscder.utils import Settings from rscder.utils.project import Project @@ -43,6 +44,18 @@ class MainWindow(QMainWindow): self.action_manager.set_status_bar(self.statusBar()) self.action_manager.set_actions() + PluginLoader(dict( + layer_tree=self.layer_tree, + pair_canvas=self.double_map, + message_box=self.message_box, + result_table=self.result_box, + project=Project(self), + mainwindow=self, + toolbar=self.toolbar, + statusbar=self.statusBar(), + **self.action_manager.menus + )).load_plugin() + self.resize(*Settings.General().size) diff --git a/rscder/gui/plugins.py b/rscder/gui/plugins.py index 098c2d9..7a80afd 100644 --- a/rscder/gui/plugins.py +++ b/rscder/gui/plugins.py @@ -1,5 +1,9 @@ +import os +import shutil from PyQt5.QtWidgets import * -from PyQt5.QtGui import QIcon, Qt +from PyQt5.QtGui import QIcon +from PyQt5.QtCore import Qt +from rscder.plugins.loader import PluginLoader from rscder.utils.setting import Settings class PluginDialog(QDialog): @@ -8,25 +12,25 @@ class PluginDialog(QDialog): super().__init__(parent) self.setWindowTitle('Plugins') self.setWindowIcon(QIcon(":/icons/logo.svg")) - self.setMinimumWidth(800) + self.setMinimumWidth(900) self.setMinimumHeight(600) - self.plugins = Settings.Plugin().plugins + self.plugins = list(Settings.Plugin().plugins) - self.plugin_table = QTableWidget(len(self.plugins), 2, self) + self.plugin_table = QTableWidget(len(self.plugins), 3, self) self.plugin_table.setSelectionMode(QAbstractItemView.ExtendedSelection) self.plugin_table.setColumnWidth(0, 200) self.plugin_table.setColumnWidth(1, 500) - self.plugin_table.setHorizontalHeaderLabels(['Name', 'Path', 'Enabled']) - self.plugin_table.setEditTriggers(QAbstractItemView.DoubleClicked) + self.plugin_table.setHorizontalHeaderLabels(['Name', 'Module', 'Enabled']) + self.plugin_table.setEditTriggers(QAbstractItemView.NoEditTriggers) self.plugin_table.cellDoubleClicked.connect(self.edit_plugin) for idx, plugin in enumerate(self.plugins): name_item = QTableWidgetItem(plugin['name']) - path_item = QTableWidgetItem(plugin['path']) + module_item = QTableWidgetItem(plugin['module']) enabled_item = QTableWidgetItem() enabled_item.setCheckState(Qt.Checked if plugin['enabled'] else Qt.Unchecked) self.plugin_table.setItem(idx, 0, name_item) - self.plugin_table.setItem(idx, 1, path_item) + self.plugin_table.setItem(idx, 1, module_item) self.plugin_table.setItem(idx, 2, enabled_item) self.add_button = QPushButton('Add', self) @@ -50,39 +54,58 @@ class PluginDialog(QDialog): self.has_change = False def add_plugin(self): - self.has_change = True - self.plugin_table.insertRow(self.plugin_table.rowCount()) + plugin_directory = QFileDialog.getExistingDirectory(self, 'Select Plugin Directory', '.') + if plugin_directory is not None: + info = PluginLoader.load_plugin_info(plugin_directory) + print(info) + + if info is not None: + info['module'] = os.path.basename(plugin_directory) + info['enabled'] = True + self.has_change = True + self.plugins.append(info) + self.plugin_table.insertRow(self.plugin_table.rowCount()) + name_item = QTableWidgetItem(info['name']) + module_item = QTableWidgetItem(info['module']) + enabled_item = QTableWidgetItem('启用') + enabled_item.setCheckState(Qt.Checked) + self.plugin_table.setItem(self.plugin_table.rowCount() - 1, 0, name_item) + self.plugin_table.setItem(self.plugin_table.rowCount() - 1, 1, module_item) + self.plugin_table.setItem(self.plugin_table.rowCount() - 1, 2, enabled_item) + + dst = PluginLoader.copy_plugin_to_3rd(plugin_directory) + if dst is not None: + self.plugins[-1]['path'] = dst + + else: + pass + def remove_plugin(self): self.has_change = True - for row in self.plugin_table.selectedItems(): - self.plugin_table.removeRow(row.row()) - + row_ids = list( row.row() for row in self.plugin_table.selectionModel().selectedRows()) + row_ids.sort(reverse=True) + for row in row_ids: + self.plugin_table.removeRow(row) + info = self.plugins.pop(row) + try: + shutil.rmtree(info['path']) + except: + pass # for idx in self.plugins def edit_plugin(self, row, column): self.has_change = True - if column == 0: - self.plugin_table.item(row, column).setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) - elif column == 1: - open_file = QFileDialog.getOpenFileName(self, 'Open File', '', 'Python Files (*.py)') - if open_file[0]: - self.plugin_table.item(row, column).setText(open_file[0]) - else: - pass - elif column == 2: + if column == 2: self.plugin_table.item(row, column).setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) - # self.plugin_list.setFixedWidth(200) def save_plugin(self): - plugins = [] for idx in range(self.plugin_table.rowCount()): - name = self.plugin_table.item(idx, 0).text() - path = self.plugin_table.item(idx, 1).text() enabled = self.plugin_table.item(idx, 2).checkState() == Qt.Checked - plugins.append({'name': name, 'path': path, 'enabled': enabled}) - Settings.Plugin().plugins = plugins + self.plugins[idx]['enabled'] = enabled + Settings.Plugin().plugins = self.plugins + self.has_change = False self.close() def closeEvent(self, event): diff --git a/rscder/mul/mulstart.py b/rscder/mul/mulstart.py index a132671..e0a4717 100644 --- a/rscder/mul/mulstart.py +++ b/rscder/mul/mulstart.py @@ -5,10 +5,10 @@ from PyQt5.QtGui import QFont, QPixmap from PyQt5.QtWidgets import QSplashScreen, QProgressBar, QStyleFactory, QMessageBox from qgis.core import QgsApplication # from qgis.core import -from gui.mainwindow import MainWindow +from rscder.gui.mainwindow import MainWindow import multiprocessing -from gui import license -from utils.setting import Settings +from rscder.gui import license +from rscder.utils.setting import Settings class MulStart: @@ -27,7 +27,7 @@ class MulStart: # pyrcc5 res.qrc -o rc.py - import rc + import rscder.rc app = QgsApplication([], True) QgsApplication.initQgis() diff --git a/rscder/plugins/basic.py b/rscder/plugins/basic.py index b57bd36..905cc32 100644 --- a/rscder/plugins/basic.py +++ b/rscder/plugins/basic.py @@ -1,6 +1,7 @@ from PyQt5.QtCore import QObject, pyqtSignal from rscder.utils.project import PairLayer + class BasicPlugin(QObject): ''' 插件基类 @@ -15,11 +16,16 @@ class BasicPlugin(QObject): statusbar: statusbar menu: menu file_menu: file menu - - ''' + @staticmethod + def info(): + ''' + Plugin info + ''' + raise NotImplementedError + def __init__(self, ctx:dict) -> None: - super().__init__() + super().__init__(ctx['mainwindow']) self.ctx = ctx self.layer_tree = ctx['layer_tree'] self.pair_canvas = ctx['pair_canvas'] @@ -29,7 +35,7 @@ class BasicPlugin(QObject): self.mainwindow = ctx['mainwindow'] self.set_action() self.project.layer_load.connect(self.on_data_load) - self.project.project_created.connect(self.setup) + self.project.project_init.connect(self.setup) def set_action(self): diff --git a/rscder/plugins/loader.py b/rscder/plugins/loader.py index 18543fc..635e133 100644 --- a/rscder/plugins/loader.py +++ b/rscder/plugins/loader.py @@ -1,4 +1,6 @@ +import shutil from rscder.utils.setting import Settings +from PyQt5.QtWidgets import QMessageBox from PyQt5.QtCore import QObject, pyqtSignal from rscder.plugins.basic import BasicPlugin import importlib @@ -11,20 +13,54 @@ class PluginLoader(QObject): plugin_loaded = pyqtSignal() def __init__(self, ctx): + super().__init__() self.ctx = ctx + self.plugins = [] + + @staticmethod + def copy_plugin_to_3rd(dir, random_suffix=True): + if not os.path.exists(Settings.Plugin().root): + os.makedirs(Settings.Plugin().root) + return shutil.copytree(dir, + os.path.join(Settings.Plugin().root, + os.path.basename(dir))) + @staticmethod + def load_plugin_info(path): + + sys.path.insert(0, os.path.join(path, '..')) + info = None + try: + module = importlib.import_module(os.path.basename(path)) + mes = inspect.getmembers(module) + for name, obj in mes: + print(name, obj) + if inspect.isclass(obj) and issubclass(obj, BasicPlugin): + info = obj.info() + break + except Exception as e: + print(e) + QMessageBox.critical(None, 'Error', f'{path} load error: {e}') + finally: + sys.path.pop(0) + return info + def load_plugin(self): plugins = Settings.Plugin().plugins + if Settings.Plugin().root not in sys.path: + sys.path.insert(0, Settings.Plugin().root) for plugin in plugins: - name = plugin['name'] - path = plugin['path'] + # path = plugin['path'] + if not plugin['enabled']: + continue try: - module = importlib.import_module(path) + module = importlib.import_module(plugin['module']) for oname, obj in inspect.getmembers(module): - if inspect.isclass(obj) and issubclass(obj, BasicPlugin) and obj != BasicPlugin and obj != PluginLoader and oname == name: - obj(self.ctx) + if inspect.isclass(obj) and issubclass(obj, BasicPlugin) and obj != BasicPlugin and obj != PluginLoader: + self.plugins.append(obj(self.ctx)) + break except Exception as e: - self.ctx['message_box'].error(f'{name} load error: {e}') - # print(e) + self.ctx['message_box'].error(f'{plugin["name"]} load error: {e}') + self.plugin_loaded.emit() \ No newline at end of file diff --git a/rscder/res.qrc b/rscder/res.qrc index 07a812c..76b52ba 100644 --- a/rscder/res.qrc +++ b/rscder/res.qrc @@ -1,26 +1,26 @@ - icons/splash.png - icons/logo.svg - icons/load.svg - icons/settings.svg - icons/exit.svg - icons/vector.svg - icons/zoomin.svg - icons/zoomout.svg - icons/pan.svg - icons/full.svg - icons/start.svg - icons/clear.svg - icons/edit.svg - icons/export.svg - icons/model.svg - icons/assessment.svg - icons/paint.svg - icons/font.svg - icons/qt.svg - icons/ok.svg - icons/cancel.svg - icons/outline.svg + icons\assessment.svg + icons\cancel.svg + icons\clear.svg + icons\edit.svg + icons\exit.svg + icons\export.svg + icons\font.svg + icons\full.svg + icons\load.svg + icons\logo.svg + icons\model.svg + icons\ok.svg + icons\outline.svg + icons\paint.svg + icons\pan.svg + icons\qt.svg + icons\settings.svg + icons\splash.png + icons\start.svg + icons\vector.svg + icons\zoomin.svg + icons\zoomout.svg diff --git a/rscder/utils/generate_rc.py b/rscder/utils/generate_rc.py new file mode 100644 index 0000000..8cd4cbb --- /dev/null +++ b/rscder/utils/generate_rc.py @@ -0,0 +1,16 @@ +import shutil +import subprocess +import os +path = os.path.dirname(os.path.realpath(__file__)) +icon_path = os.path.join(path, '..', 'icons') +with open(os.path.join(path, '..', 'res.qrc'), 'w') as f: + f.write(f'\n') + f.write(f' \n') + for icon in os.listdir(icon_path): + f.write(f' {os.path.join("icons", icon)}\n') + f.write(f' \n') + f.write(f'\n') + +subprocess.run(['pyrcc5', 'res.qrc', '-o', 'rc.py'], cwd=os.path.join(path, '..')) + +shutil.rmtree(icon_path) \ No newline at end of file diff --git a/rscder/utils/project.py b/rscder/utils/project.py index 26853a8..b0b5c9e 100644 --- a/rscder/utils/project.py +++ b/rscder/utils/project.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import Dict, List import uuid from osgeo import gdal, gdal_array -from utils.setting import Settings +from rscder.utils.setting import Settings from qgis.core import QgsRasterLayer, QgsLineSymbol, QgsSingleSymbolRenderer, QgsSimpleLineSymbolLayer, QgsVectorLayer, QgsCoordinateReferenceSystem, QgsFeature, QgsGeometry, QgsPointXY from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtGui import QColor diff --git a/rscder/utils/setting.py b/rscder/utils/setting.py index 0ea5673..c32103c 100644 --- a/rscder/utils/setting.py +++ b/rscder/utils/setting.py @@ -27,8 +27,10 @@ class Settings(QSettings): @property def plugins(self): with Settings(Settings.Plugin.PRE) as s: - return s.value('plugins', []) - + pl = s.value('plugins', []) + if pl is None: + return [] + return pl @plugins.setter def plugins(self, value): with Settings(Settings.Plugin.PRE) as s: diff --git a/main.py b/run.py similarity index 100% rename from main.py rename to run.py