easyPlugin.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. # -*- coding: utf-8 -*-
  2. """
  3. /***************************************************************************
  4. Plugin which creates a simple QGIS plugin templates ready for install, editing and testing
  5. -------------------
  6. released : 2024-02-28
  7. author : (C) 2024 by Pavel Pereverzev
  8. email : pavelpereverzev93@gmail.com
  9. made in : easyPlugin by Pavel Pereverzev
  10. credits to : Gary Sherman and Alexandre Neto
  11. ***************************************************************************/
  12. /***************************************************************************
  13. * *
  14. * This program is free software; you can redistribute it and/or modify *
  15. * it under the terms of the GNU General Public License as published by *
  16. * the Free Software Foundation; either version 2 of the License, or *
  17. * (at your option) any later version. *
  18. * *
  19. ***************************************************************************/
  20. """
  21. # import system, PyQt and QGIS libraries
  22. from __future__ import absolute_import
  23. import os
  24. from datetime import datetime
  25. import shutil
  26. import zipfile
  27. from PyQt5 import QtCore
  28. from PyQt5.QtCore import *
  29. from PyQt5.QtGui import *
  30. from PyQt5.QtWidgets import *
  31. from qgis.core import *
  32. from qgis._gui import *
  33. from qgis.utils import iface
  34. import pyplugin_installer
  35. from .easyScripter import *
  36. # variables
  37. # plugin name validator
  38. rx = QRegExp("[A-Za-z_ ]*")
  39. validator = QRegExpValidator(rx)
  40. script_folder = os.path.dirname(os.path.realpath(__file__))
  41. # links to original data
  42. file_mdata = os.path.join(script_folder, "template_data", "metadata.txt")
  43. file_init = os.path.join(script_folder, "template_data", "__init__.txt")
  44. file_action = os.path.join(script_folder, "template_data", "template.txt")
  45. file_tools = os.path.join(script_folder, "template_data", "template_tools.txt")
  46. file_icon = os.path.join(script_folder, "template_data", "icon.png")
  47. ptypes = {
  48. "Action": ["self.simple_action()", False, "icon_script.png"],
  49. "Widget": ["self.simple_gui()", False, "icon_widget.png"],
  50. "Map tool": ["self.simple_map_tool()", True, "icon_maptool.png"],
  51. "Custom": ["self.custom_tool()", False, "icon_custom.png"],
  52. }
  53. # current date/year
  54. curr_day = datetime.strftime(datetime.now(), "%Y-%m-%d")
  55. curr_year = datetime.now().year
  56. def prepare_data(
  57. folder_out,
  58. plugin_name,
  59. plugin_classname,
  60. plugin_type,
  61. plugin_custom_file,
  62. plugin_desc,
  63. plugin_author,
  64. plugin_mail,
  65. plugin_site,
  66. icon_path,
  67. ):
  68. global file_icon
  69. # creating out paths
  70. folder_out_sub = os.path.join(folder_out, plugin_classname)
  71. if not os.path.isdir(folder_out_sub):
  72. os.mkdir(folder_out_sub)
  73. file_new_mdata = os.path.join(folder_out_sub, r"metadata.txt")
  74. file_new_init = os.path.join(folder_out_sub, r"__init__.py")
  75. file_new_action = os.path.join(folder_out_sub, r"{}.py".format(plugin_classname))
  76. file_new_tools = os.path.join(folder_out_sub, r"template_tools.py")
  77. file_new_icon = os.path.join(folder_out_sub, r"icon.png")
  78. file_new_custom = os.path.join(
  79. os.path.dirname(script_folder),
  80. plugin_name,
  81. os.path.basename(plugin_custom_file),
  82. )
  83. file_new_custom_local = os.path.join(
  84. folder_out_sub, os.path.basename(plugin_custom_file)
  85. )
  86. cmd_custom = (
  87. "pass"
  88. if not plugin_custom_file
  89. else """self.result = exec(open(r'{}'.encode('utf-8')).read(), {{"wrapper": self}})""".format(
  90. file_new_custom
  91. )
  92. )
  93. # 1. metadata
  94. with open(file_mdata, "r", encoding="utf-8") as mdata_obj:
  95. mdata = mdata_obj.read()
  96. mdata_edited = mdata.format(
  97. plugin_name,
  98. plugin_desc,
  99. "", # plugin_about,
  100. plugin_author,
  101. plugin_mail,
  102. plugin_site,
  103. )
  104. with open(file_new_mdata, "w", encoding="utf-8") as mdata_obj:
  105. mdata_obj.write(mdata_edited)
  106. # 2. init
  107. with open(file_init, "r", encoding="utf-8") as mdata_obj:
  108. idata = mdata_obj.read()
  109. idata_edited = idata.format(
  110. plugin_name,
  111. plugin_desc,
  112. curr_day,
  113. curr_year,
  114. plugin_author,
  115. plugin_mail,
  116. plugin_classname,
  117. )
  118. with open(file_new_init, "w", encoding="utf-8") as mdata_obj:
  119. mdata_obj.write(idata_edited)
  120. # 3. class
  121. with open(file_action, "r", encoding="utf-8") as mdata_obj:
  122. py_data = mdata_obj.read()
  123. py_data_edited = py_data.format(
  124. plugin_desc,
  125. curr_day,
  126. curr_year,
  127. plugin_author,
  128. plugin_mail,
  129. plugin_classname,
  130. plugin_name,
  131. ptypes[plugin_type][0],
  132. ptypes[plugin_type][1],
  133. cmd_custom,
  134. )
  135. with open(file_new_action, "w", encoding="utf-8") as mdata_obj:
  136. mdata_obj.write(py_data_edited)
  137. # 4. icon
  138. if icon_path:
  139. file_icon = icon_path
  140. else:
  141. file_icon = os.path.join(script_folder, "icons", ptypes[plugin_type][2])
  142. shutil.copyfile(file_icon, file_new_icon)
  143. # 5. tools
  144. shutil.copyfile(file_tools, file_new_tools)
  145. # 6 custom tool
  146. if plugin_custom_file:
  147. shutil.copyfile(plugin_custom_file, file_new_custom_local)
  148. # 7. making a zip
  149. out_file = os.path.join(
  150. os.path.dirname(folder_out_sub), "{}.zip".format(plugin_name)
  151. )
  152. zf = zipfile.ZipFile(out_file, "w")
  153. for filename in os.listdir(folder_out_sub):
  154. full_file_path = os.path.join(folder_out_sub, filename)
  155. fl = os.path.basename(os.path.dirname(full_file_path))
  156. new_path = os.path.join(fl, filename)
  157. zf.write(full_file_path, arcname=new_path)
  158. zf.close()
  159. return out_file
  160. def remove_docked_widgets():
  161. for ch in iface.mainWindow().children():
  162. if "om_mod" in ch.objectName():
  163. ch.close()
  164. ch = None
  165. class easyWidget(QWidget):
  166. def __init__(self, parent=None):
  167. super().__init__()
  168. self.setWindowTitle("easyPlugin by Pavel Pereverzev")
  169. self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
  170. self.setGeometry(700, 500, 400, 100)
  171. # attrs
  172. self.save_path = None
  173. self.icon_path = None
  174. self.plugin_name = None
  175. self.plugin_desc = None
  176. self.plugin_about = None
  177. self.plugin_author = None
  178. self.plugin_mail = None
  179. self.class_file = None
  180. self.class_name = None
  181. # layout
  182. groupbox_about = QGroupBox("Main parameters")
  183. groupbox_misc = QGroupBox("Misc.")
  184. vbox = QVBoxLayout(self)
  185. grid_about = QGridLayout()
  186. grid_about.setSpacing(10)
  187. grid_misc = QGridLayout()
  188. grid_misc.setSpacing(10)
  189. self.l_pname = QLabel("Plugin title")
  190. self.line_pname = QLineEdit(self)
  191. # self.line_pname.setValidator(validator)
  192. self.l_type = QLabel("Plugin type")
  193. self.plugin_type = QComboBox()
  194. self.plugin_type.addItems(["Action", "Widget", "Map tool", "Custom"])
  195. self.l_custom = QLabel("Custom file")
  196. self.line_custom = QLineEdit()
  197. self.line_custom.setReadOnly(True)
  198. self.btn_custom = QPushButton(self)
  199. self.btn_custom.setIcon(self.style().standardIcon(QStyle.SP_DirIcon))
  200. self.line_custom.setDisabled(True)
  201. self.btn_custom.setDisabled(True)
  202. self.l_desc = QLabel("Plugin\ndescription")
  203. self.line_desc = QLineEdit(self)
  204. # self.l_about = QLabel('About')
  205. # self.line_about = QLineEdit(self)
  206. self.l_author = QLabel("Author")
  207. self.line_author = QLineEdit(self)
  208. self.l_mail = QLabel("E-mail")
  209. self.line_mail = QLineEdit(self)
  210. self.l_site = QLabel("Site/GitHub")
  211. self.line_site = QLineEdit(self)
  212. self.l_folder = QLabel("Out folder")
  213. self.line_folder = QLineEdit(self)
  214. self.line_folder.setReadOnly(True)
  215. self.btn_folder = QPushButton(self)
  216. self.btn_folder.setIcon(self.style().standardIcon(QStyle.SP_DirIcon))
  217. self.btn_folder.setMaximumWidth(30)
  218. self.l_icon = QLabel("Icon")
  219. self.line_icon = QLineEdit(self)
  220. self.line_icon.setReadOnly(True)
  221. self.btn_icon = QPushButton(self)
  222. self.btn_icon.setIcon(self.style().standardIcon(QStyle.SP_DirIcon))
  223. self.btn_icon.setMaximumWidth(30)
  224. self.btn_generate = QPushButton("Generate plugin")
  225. self.btn_generate.setMinimumHeight(32)
  226. # gui setup
  227. grid_about.addWidget(self.l_pname, 0, 1, 1, 1)
  228. grid_about.addWidget(self.line_pname, 0, 2, 1, 4)
  229. grid_about.addWidget(self.l_type, 1, 1, 1, 1)
  230. grid_about.addWidget(self.plugin_type, 1, 2, 1, 4)
  231. grid_about.addWidget(self.l_custom, 2, 1, 1, 1)
  232. grid_about.addWidget(self.line_custom, 2, 2, 1, 3)
  233. grid_about.addWidget(self.btn_custom, 2, 5, 1, 1)
  234. grid_about.addWidget(self.l_desc, 3, 1, 1, 1)
  235. grid_about.addWidget(self.line_desc, 3, 2, 1, 4)
  236. grid_about.addWidget(self.l_author, 4, 1, 1, 1)
  237. grid_about.addWidget(self.line_author, 4, 2, 1, 4)
  238. grid_about.addWidget(self.l_mail, 5, 1, 1, 1)
  239. grid_about.addWidget(self.line_mail, 5, 2, 1, 4)
  240. grid_about.addWidget(self.l_site, 6, 1, 1, 1)
  241. grid_about.addWidget(self.line_site, 6, 2, 1, 4)
  242. grid_about.addWidget(self.l_folder, 7, 1, 1, 1)
  243. grid_about.addWidget(self.line_folder, 7, 2, 1, 3)
  244. grid_about.addWidget(self.btn_folder, 7, 5, 1, 1)
  245. grid_misc.addWidget(self.l_icon, 0, 1, 1, 1)
  246. grid_misc.addWidget(self.line_icon, 0, 2, 1, 3)
  247. grid_misc.addWidget(self.btn_icon, 0, 5, 1, 1)
  248. grid_about.setColumnStretch(2, 3)
  249. grid_misc.setColumnStretch(2, 3)
  250. groupbox_about.setLayout(grid_about)
  251. groupbox_misc.setLayout(grid_misc)
  252. vbox.addWidget(groupbox_about)
  253. vbox.addWidget(groupbox_misc)
  254. vbox.addWidget(self.btn_generate)
  255. self.setLayout(vbox)
  256. self.plugin_type.currentTextChanged.connect(self.check_plugin_type)
  257. self.btn_custom.clicked.connect(self.select_custom_file)
  258. self.btn_folder.clicked.connect(self.select_folder)
  259. self.btn_generate.clicked.connect(self.generate_plugin)
  260. self.line_pname.textChanged.connect(self.check_text)
  261. self.btn_icon.clicked.connect(self.select_icon)
  262. self.show()
  263. def check_plugin_type(self):
  264. ptype = self.plugin_type.currentText()
  265. if ptype == "Custom":
  266. self.line_custom.setDisabled(False)
  267. self.btn_custom.setDisabled(False)
  268. else:
  269. self.line_custom.setDisabled(True)
  270. self.btn_custom.setDisabled(True)
  271. return
  272. def select_custom_file(self):
  273. # select icon for a plugin
  274. result_file = QFileDialog.getOpenFileName(
  275. self, "Select python script file", None, "Python files (*.py)"
  276. )[0]
  277. if result_file:
  278. self.line_custom.setText(result_file)
  279. def select_icon(self):
  280. # select icon for a plugin
  281. result_file = QFileDialog.getOpenFileName(
  282. self, "Select icon picture", None, "Images (*.png)"
  283. )[0]
  284. if result_file:
  285. self.line_icon.setText(result_file)
  286. self.icon_path = result_file
  287. def check_text(self):
  288. # validate input plugin name
  289. global_pos = self.line_pname.mapToGlobal(self.line_pname.rect().topRight())
  290. curr_v = self.line_pname.text()
  291. v = validator.validate(curr_v, 0)[0]
  292. if v != QValidator.Acceptable:
  293. QToolTip.showText(
  294. global_pos,
  295. "Only english characters\nand _ symbol are allowed",
  296. self.line_pname,
  297. )
  298. self.line_pname.setStyleSheet("QLineEdit {background: #ffc8c8;}")
  299. pass
  300. else:
  301. self.line_pname.setStyleSheet("")
  302. pass
  303. def warning_message(self, err_text):
  304. # custom warning message
  305. msg = QMessageBox()
  306. msg.warning(self, "Warning", err_text)
  307. return
  308. def question_message(self, question_text):
  309. final_question = QMessageBox(self)
  310. answer = final_question.question(
  311. self, "easyPlugin", question_text, final_question.Yes | final_question.No
  312. )
  313. return answer
  314. def select_folder(self):
  315. # path to save plugin
  316. result = QFileDialog.getExistingDirectory(None, "Select folder to save plugin")
  317. self.save_path = result
  318. self.line_folder.setText(self.save_path)
  319. return
  320. def generate_plugin(self):
  321. # get all inputs and generate pl
  322. self.plugin_name = self.line_pname.text()
  323. self.class_name = self.plugin_name.replace(" ", "_")
  324. self.ptype = self.plugin_type.currentText()
  325. self.custom_file = self.line_custom.text()
  326. self.plugin_desc = self.line_desc.text()
  327. # self.plugin_about = self.line_about.text()
  328. self.plugin_author = self.line_author.text()
  329. self.plugin_mail = self.line_mail.text()
  330. self.plugin_site = self.line_site.text()
  331. v = validator.validate(self.line_pname.text(), 0)[0] == QValidator.Acceptable
  332. if all([v, self.save_path]):
  333. plugin_file = prepare_data(
  334. self.save_path,
  335. self.plugin_name,
  336. self.class_name,
  337. self.ptype,
  338. self.custom_file,
  339. self.plugin_desc,
  340. self.plugin_author,
  341. self.plugin_mail,
  342. self.plugin_site,
  343. self.icon_path,
  344. )
  345. user_answer = self.question_message(
  346. "Plugin {} is good to go.\nInstall it now?".format(self.plugin_name)
  347. )
  348. if user_answer == QMessageBox.Yes:
  349. pyplugin_installer.instance().installFromZipFile(plugin_file)
  350. self.warning_message("Plugin installed!")
  351. else:
  352. pass
  353. else:
  354. self.warning_message("Plugin title and Out folder should be specified")
  355. class easyPlugin(object):
  356. # main plugin class
  357. def __init__(self, iface):
  358. # Save reference to the QGIS interface
  359. self.iface = iface
  360. # initialize plugin directory
  361. self.plugin_dir = os.path.dirname(__file__)
  362. # initialize locale
  363. locale = QSettings().value("locale/userLocale")[0:2]
  364. locale_path = os.path.join(
  365. self.plugin_dir, "i18n", "easyPlugin_{}.qm".format(locale)
  366. )
  367. if os.path.exists(locale_path):
  368. self.translator = QTranslator()
  369. self.translator.load(locale_path)
  370. QCoreApplication.installTranslator(self.translator)
  371. # Declare instance attributes
  372. self.actions = []
  373. self.menu = self.tr("easyPlugin")
  374. # Check if plugin was started the first time in current QGIS session
  375. # Must be set in initGui() to survive plugin reloads
  376. self.first_start = None
  377. def initGui(self):
  378. # Create the menu entries and toolbar icons inside the QGIS GUI
  379. icon_path = QIcon(os.path.join(self.plugin_dir, "icon.png"))
  380. icon_path_scripter = QIcon(os.path.join(self.plugin_dir, "icon_scripter.png"))
  381. self.icon_action = self.add_action(
  382. icon_path,
  383. text=self.tr("easyPlugin"),
  384. callback=self.run,
  385. checkable=False,
  386. parent=self.iface.mainWindow(),
  387. )
  388. self.icon_action_scripter = self.add_action(
  389. icon_path_scripter,
  390. text=self.tr("easyScripter"),
  391. callback=self.runScripter,
  392. checkable=True,
  393. parent=self.iface.mainWindow(),
  394. )
  395. self.scripterIsActive = False
  396. self.dockwidget = None
  397. # will be set False in run()
  398. self.first_start = True
  399. def add_action(
  400. self,
  401. icon_path,
  402. text,
  403. callback,
  404. enabled_flag=True,
  405. checkable=False,
  406. add_to_menu=False,
  407. add_to_toolbar=True,
  408. status_tip=None,
  409. whats_this=None,
  410. parent=None,
  411. ):
  412. icon = QIcon(icon_path)
  413. action = QAction(icon, text, parent)
  414. action.triggered.connect(callback)
  415. action.setEnabled(enabled_flag)
  416. action.setCheckable(checkable)
  417. if status_tip is not None:
  418. action.setStatusTip(status_tip)
  419. if whats_this is not None:
  420. action.setWhatsThis(whats_this)
  421. if add_to_toolbar:
  422. # Adds plugin icon to Plugins toolbar
  423. self.iface.addToolBarIcon(action)
  424. if add_to_menu:
  425. self.iface.addPluginToMenu(self.menu, action)
  426. self.actions.append(action)
  427. return action
  428. def unload(self):
  429. # Removes the plugin menu item and icon from QGIS GUI
  430. for action in self.actions:
  431. self.iface.removeToolBarIcon(action)
  432. def tr(self, text):
  433. return QCoreApplication.translate("easyPlugin", text)
  434. # MAIN ACTION FUNCTION IS HERE
  435. def run(self):
  436. # run method that performs all the real work
  437. self.app = easyWidget()
  438. def runScripter(self):
  439. if not self.scripterIsActive:
  440. self.scripterIsActive = True
  441. if self.dockwidget == None:
  442. self.dockwidget = Scripter(None)
  443. self.dockwidget.closingPlugin.connect(self.onClosePlugin)
  444. self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dockwidget)
  445. else:
  446. self.dockwidget.close()
  447. def onClosePlugin(self):
  448. self.dockwidget.closingPlugin.disconnect(self.onClosePlugin)
  449. self.dockwidget = None
  450. self.scripterIsActive = False
  451. self.icon_action_scripter.setChecked(False)