ObjectCollection.py 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # Author: Juan Pablo Caram (c) #
  5. # Date: 2/5/2014 #
  6. # MIT Licence #
  7. # ##########################################################
  8. # ##########################################################
  9. # File modified by: Dennis Hayrullin #
  10. # File modified by: Marius Stanciu #
  11. # ##########################################################
  12. # ##########################################################
  13. # File Modified : Marcos Dumay de Medeiros #
  14. # Modifications under GPLv3 #
  15. # ##########################################################
  16. from PyQt5 import QtGui, QtCore, QtWidgets
  17. from PyQt5.QtCore import Qt, QSettings
  18. from PyQt5.QtGui import QColor
  19. # from PyQt5.QtCore import QModelIndex
  20. from appObjects.FlatCAMObj import FlatCAMObj
  21. from appObjects.FlatCAMCNCJob import CNCJobObject
  22. from appObjects.FlatCAMDocument import DocumentObject
  23. from appObjects.FlatCAMExcellon import ExcellonObject
  24. from appObjects.FlatCAMGeometry import GeometryObject
  25. from appObjects.FlatCAMGerber import GerberObject
  26. from appObjects.FlatCAMScript import ScriptObject
  27. import inspect # TODO: Remove
  28. import re
  29. import logging
  30. from copy import deepcopy
  31. from numpy import inf
  32. import gettext
  33. import appTranslation as fcTranslate
  34. import builtins
  35. fcTranslate.apply_language('strings')
  36. if '_' not in builtins.__dict__:
  37. _ = gettext.gettext
  38. log = logging.getLogger('base')
  39. class KeySensitiveListView(QtWidgets.QTreeView):
  40. """
  41. QtGui.QListView extended to emit a signal on key press.
  42. """
  43. def __init__(self, app, parent=None):
  44. super(KeySensitiveListView, self).__init__(parent)
  45. self.setHeaderHidden(True)
  46. # self.setRootIsDecorated(False)
  47. # self.setExpandsOnDoubleClick(False)
  48. self.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers) # No edit in the Project Tab Tree
  49. # Enable dragging and dropping onto the appGUI
  50. self.setAcceptDrops(True)
  51. self.filename = ""
  52. self.app = app
  53. # Enabling Drag and Drop for the items in the Project Tab
  54. # Example: https://github.com/d1vanov/PyQt5-reorderable-list-model/blob/master/reorderable_list_model.py
  55. # https://github.com/jimmykuu/PyQt-PySide-Cookbook/blob/master/tree/drop_indicator.md
  56. # self.setDragEnabled(True)
  57. # self.viewport().setAcceptDrops(True)
  58. # self.setDropIndicatorShown(True)
  59. # self.DragDropMode(QtWidgets.QAbstractItemView.InternalMove)
  60. # self.current_idx = None
  61. # self.current_group = None
  62. # self.dropped_obj = None
  63. keyPressed = QtCore.pyqtSignal(int)
  64. def keyPressEvent(self, event):
  65. # super(KeySensitiveListView, self).keyPressEvent(event)
  66. self.keyPressed.emit(event.key())
  67. def dragEnterEvent(self, event):
  68. # if event.source():
  69. # self.current_idx = self.currentIndex()
  70. # self.current_group = self.model().group_items[self.current_idx.internalPointer().obj.kind]
  71. # self.dropped_obj = self.current_idx.internalPointer().obj
  72. if event.mimeData().hasUrls:
  73. event.accept()
  74. else:
  75. event.ignore()
  76. def dragMoveEvent(self, event):
  77. self.setDropIndicatorShown(True)
  78. if event.mimeData().hasUrls:
  79. event.accept()
  80. else:
  81. event.ignore()
  82. def dropEvent(self, event):
  83. drop_indicator = self.dropIndicatorPosition()
  84. # if event.source():
  85. # new_index = self.indexAt(event.pos())
  86. # new_group = self.model().group_items[new_index.internalPointer().obj.kind]
  87. # if self.current_group == new_group:
  88. #
  89. # # delete it from the model
  90. # deleted_obj_name = self.dropped_obj.options['name']
  91. # self.model().delete_by_name(deleted_obj_name)
  92. #
  93. # # add the object to the new index
  94. # self.model().append(self.dropped_obj, to_index=new_index)
  95. #
  96. # return
  97. m = event.mimeData()
  98. if m.hasUrls:
  99. event.accept()
  100. for url in m.urls():
  101. self.filename = str(url.toLocalFile())
  102. # file drop from outside application
  103. if drop_indicator == QtWidgets.QAbstractItemView.OnItem:
  104. if self.filename == "":
  105. self.app.inform.emit(_("Cancelled."))
  106. else:
  107. if self.filename.lower().rpartition('.')[-1] in self.app.grb_list:
  108. self.app.worker_task.emit({'fcn': self.app.f_handlers.open_gerber,
  109. 'params': [self.filename]})
  110. else:
  111. event.ignore()
  112. if self.filename.lower().rpartition('.')[-1] in self.app.exc_list:
  113. self.app.worker_task.emit({'fcn': self.app.f_handlers.open_excellon,
  114. 'params': [self.filename]})
  115. else:
  116. event.ignore()
  117. if self.filename.lower().rpartition('.')[-1] in self.app.gcode_list:
  118. self.app.worker_task.emit({'fcn': self.app.f_handlers.open_gcode,
  119. 'params': [self.filename]})
  120. else:
  121. event.ignore()
  122. if self.filename.lower().rpartition('.')[-1] in self.app.svg_list:
  123. object_type = 'geometry'
  124. self.app.worker_task.emit({'fcn': self.app.f_handlers.import_svg,
  125. 'params': [self.filename, object_type, None]})
  126. if self.filename.lower().rpartition('.')[-1] in self.app.dxf_list:
  127. object_type = 'geometry'
  128. self.app.worker_task.emit({'fcn': self.app.f_handlers.import_dxf,
  129. 'params': [self.filename, object_type, None]})
  130. if self.filename.lower().rpartition('.')[-1] in self.app.prj_list:
  131. # self.app.open_project() is not Thread Safe
  132. self.app.f_handlers.open_project(self.filename)
  133. else:
  134. event.ignore()
  135. else:
  136. pass
  137. else:
  138. event.ignore()
  139. class TreeItem(KeySensitiveListView):
  140. """
  141. Item of a tree model
  142. """
  143. def __init__(self, data, icon=None, obj=None, parent_item=None):
  144. super(TreeItem, self).__init__(parent_item)
  145. self.parent_item = parent_item
  146. self.item_data = data # Columns string data
  147. self.icon = icon # Decoration
  148. self.obj = obj # FlatCAMObj
  149. self.child_items = []
  150. if parent_item:
  151. parent_item.append_child(self)
  152. def append_child(self, item):
  153. self.child_items.append(item)
  154. item.set_parent_item(self)
  155. def remove_child(self, item):
  156. child = self.child_items.pop(self.child_items.index(item))
  157. child.obj.clear(True)
  158. child.obj.delete()
  159. del child.obj
  160. del child
  161. def remove_children(self):
  162. for child in self.child_items:
  163. child.obj.clear()
  164. child.obj.delete()
  165. del child.obj
  166. del child
  167. self.child_items = []
  168. def child(self, row):
  169. return self.child_items[row]
  170. def child_count(self):
  171. return len(self.child_items)
  172. def column_count(self):
  173. return len(self.item_data)
  174. def data(self, column):
  175. return self.item_data[column]
  176. def row(self):
  177. return self.parent_item.child_items.index(self)
  178. def set_parent_item(self, parent_item):
  179. self.parent_item = parent_item
  180. def __del__(self):
  181. del self.icon
  182. class ObjectCollection(QtCore.QAbstractItemModel):
  183. """
  184. Object storage and management.
  185. """
  186. groups = [
  187. ("gerber", _("Gerber")),
  188. ("excellon", _("Excellon")),
  189. ("geometry", _("Geometry")),
  190. ("cncjob", "CNC Job"),
  191. ("script", _("Script")),
  192. ("document", _("Document")),
  193. ]
  194. classdict = {
  195. "gerber": GerberObject,
  196. "excellon": ExcellonObject,
  197. "cncjob": CNCJobObject,
  198. "geometry": GeometryObject,
  199. "script": ScriptObject,
  200. "document": DocumentObject
  201. }
  202. icon_files = {
  203. "gerber": "assets/resources/flatcam_icon16.png",
  204. "excellon": "assets/resources/drill16.png",
  205. "cncjob": "assets/resources/cnc16.png",
  206. "geometry": "assets/resources/geometry16.png",
  207. "script": "assets/resources/script_new16.png",
  208. "document": "assets/resources/notes16_1.png"
  209. }
  210. # will emit the name of the object that was just selected
  211. item_selected = QtCore.pyqtSignal(str)
  212. update_list_signal = QtCore.pyqtSignal()
  213. root_item = None
  214. # app = None
  215. def __init__(self, app, parent=None):
  216. QtCore.QAbstractItemModel.__init__(self)
  217. self.app = app
  218. # ## Icons for the list view
  219. self.icons = {}
  220. for kind in ObjectCollection.icon_files:
  221. self.icons[kind] = QtGui.QPixmap(
  222. ObjectCollection.icon_files[kind].replace('assets/resources', self.app.resource_location))
  223. # Create root tree view item
  224. self.root_item = TreeItem(["root"])
  225. # Create group items
  226. self.group_items = {}
  227. for kind, title in ObjectCollection.groups:
  228. item = TreeItem([title], self.icons[kind])
  229. self.group_items[kind] = item
  230. self.root_item.append_child(item)
  231. # Create test sub-items
  232. # for i in self.root_item.m_child_items:
  233. # print i.data(0)
  234. # i.append_child(TreeItem(["empty"]))
  235. # ## Data # ##
  236. self.checked_indexes = []
  237. # Names of objects that are expected to become available.
  238. # For example, when the creation of a new object will run
  239. # in the background and will complete some time in the
  240. # future. This is a way to reserve the name and to let other
  241. # tasks know that they have to wait until available.
  242. self.promises = set()
  243. # same as above only for objects that are plotted
  244. self.plot_promises = set()
  245. # ## View
  246. self.view = KeySensitiveListView(self.app)
  247. self.view.setModel(self)
  248. self.view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
  249. self.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
  250. if self.app.defaults["global_allow_edit_in_project_tab"] is True:
  251. self.view.setEditTriggers(QtWidgets.QTreeView.SelectedClicked) # allow Edit on Tree
  252. else:
  253. self.view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
  254. # self.view.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
  255. # self.view.setDragEnabled(True)
  256. # self.view.setAcceptDrops(True)
  257. # self.view.setDropIndicatorShown(True)
  258. settings = QSettings("Open Source", "FlatCAM")
  259. if settings.contains("notebook_font_size"):
  260. fsize = settings.value('notebook_font_size', type=int)
  261. else:
  262. fsize = 12
  263. font = QtGui.QFont()
  264. font.setPixelSize(fsize)
  265. font.setFamily("Seagoe UI")
  266. self.view.setFont(font)
  267. # ## GUI Events
  268. self.view.selectionModel().selectionChanged.connect(self.on_list_selection_change)
  269. # self.view.activated.connect(self.on_item_activated)
  270. self.view.keyPressed.connect(self.app.ui.keyPressEvent)
  271. # self.view.clicked.connect(self.on_mouse_down)
  272. self.view.customContextMenuRequested.connect(self.on_menu_request)
  273. self.click_modifier = None
  274. self.update_list_signal.connect(self.on_update_list_signal)
  275. self.view.activated.connect(self.on_row_activated)
  276. self.item_selected.connect(self.on_row_selected)
  277. def promise(self, obj_name):
  278. log.debug("Object %s has been promised." % obj_name)
  279. self.promises.add(obj_name)
  280. def has_promises(self):
  281. return len(self.promises) > 0
  282. def plot_promise(self, plot_obj_name):
  283. self.plot_promises.add(plot_obj_name)
  284. def plot_remove_promise(self, plot_obj_name):
  285. if plot_obj_name in self.plot_promises:
  286. self.plot_promises.remove(plot_obj_name)
  287. def has_plot_promises(self):
  288. return len(self.plot_promises) > 0
  289. def on_mouse_down(self, event):
  290. log.debug("Mouse button pressed on list")
  291. def on_menu_request(self, pos):
  292. sel = len(self.view.selectedIndexes()) > 0
  293. self.app.ui.menuprojectenable.setEnabled(sel)
  294. self.app.ui.menuprojectdisable.setEnabled(sel)
  295. self.app.ui.menuprojectcolor.setEnabled(sel)
  296. self.app.ui.menuprojectviewsource.setEnabled(sel)
  297. self.app.ui.menuprojectcopy.setEnabled(sel)
  298. self.app.ui.menuprojectedit.setEnabled(sel)
  299. self.app.ui.menuprojectdelete.setEnabled(sel)
  300. self.app.ui.menuprojectsave.setEnabled(sel)
  301. self.app.ui.menuprojectproperties.setEnabled(sel)
  302. if sel:
  303. self.app.ui.menuprojectgeneratecnc.setVisible(True)
  304. self.app.ui.menuprojectedit.setVisible(True)
  305. self.app.ui.menuprojectsave.setVisible(True)
  306. self.app.ui.menuprojectviewsource.setVisible(True)
  307. self.app.ui.menuprojectcolor.setEnabled(False)
  308. for obj in self.get_selected():
  309. if type(obj) == GerberObject or type(obj) == ExcellonObject:
  310. self.app.ui.menuprojectcolor.setEnabled(True)
  311. if type(obj) != GeometryObject:
  312. self.app.ui.menuprojectgeneratecnc.setVisible(False)
  313. # if type(obj) != GeometryObject and type(obj) != ExcellonObject and type(obj) != GerberObject or \
  314. # type(obj) != CNCJobObject:
  315. # self.app.ui.menuprojectedit.setVisible(False)
  316. if type(obj) != GerberObject and type(obj) != ExcellonObject and type(obj) != CNCJobObject:
  317. self.app.ui.menuprojectviewsource.setVisible(False)
  318. if type(obj) != GerberObject and type(obj) != GeometryObject and type(obj) != ExcellonObject and \
  319. type(obj) != CNCJobObject:
  320. # meaning for Scripts and for Document type of FlatCAM object
  321. self.app.ui.menuprojectenable.setVisible(False)
  322. self.app.ui.menuprojectdisable.setVisible(False)
  323. self.app.ui.menuprojectedit.setVisible(False)
  324. self.app.ui.menuprojectproperties.setVisible(False)
  325. self.app.ui.menuprojectgeneratecnc.setVisible(False)
  326. else:
  327. self.app.ui.menuprojectgeneratecnc.setVisible(False)
  328. self.app.ui.menuproject.popup(self.view.mapToGlobal(pos))
  329. def index(self, row, column=0, parent=None, *args, **kwargs):
  330. if not self.hasIndex(row, column, parent):
  331. return QtCore.QModelIndex()
  332. # if not parent.isValid():
  333. # parent_item = self.root_item
  334. # else:
  335. # parent_item = parent.internalPointer()
  336. parent_item = parent.internalPointer() if parent.isValid() else self.root_item
  337. child_item = parent_item.child(row)
  338. if child_item:
  339. return self.createIndex(row, column, child_item)
  340. else:
  341. return QtCore.QModelIndex()
  342. def parent(self, index=None):
  343. if not index.isValid():
  344. return QtCore.QModelIndex()
  345. parent_item = index.internalPointer().parent_item
  346. if parent_item == self.root_item:
  347. return QtCore.QModelIndex()
  348. return self.createIndex(parent_item.row(), 0, parent_item)
  349. def rowCount(self, index=None, *args, **kwargs):
  350. if index.column() > 0:
  351. return 0
  352. if not index.isValid():
  353. parent_item = self.root_item
  354. else:
  355. parent_item = index.internalPointer()
  356. return parent_item.child_count()
  357. def columnCount(self, index=None, *args, **kwargs):
  358. if index.isValid():
  359. return index.internalPointer().column_count()
  360. else:
  361. return self.root_item.column_count()
  362. def data(self, index, role=None):
  363. if not index.isValid():
  364. return None
  365. if role in [Qt.DisplayRole, Qt.EditRole]:
  366. obj = index.internalPointer().obj
  367. if obj:
  368. return obj.options["name"]
  369. else:
  370. return index.internalPointer().data(index.column())
  371. if role == Qt.ForegroundRole:
  372. color = QColor(self.app.defaults['global_proj_item_color'])
  373. color_disabled = QColor(self.app.defaults['global_proj_item_dis_color'])
  374. obj = index.internalPointer().obj
  375. if obj:
  376. return QtGui.QBrush(color) if obj.options["plot"] else QtGui.QBrush(color_disabled)
  377. else:
  378. return index.internalPointer().data(index.column())
  379. elif role == Qt.DecorationRole:
  380. icon = index.internalPointer().icon
  381. if icon:
  382. return icon
  383. else:
  384. return QtGui.QPixmap()
  385. elif role == Qt.ToolTipRole:
  386. try:
  387. obj = index.internalPointer().obj
  388. except AttributeError:
  389. return None
  390. if obj:
  391. text = obj.options['name']
  392. return text
  393. else:
  394. QtWidgets.QToolTip.hideText()
  395. return None
  396. else:
  397. return None
  398. def setData(self, index, data, role=None):
  399. if index.isValid():
  400. obj = index.internalPointer().obj
  401. if obj:
  402. old_name = deepcopy(obj.options['name'])
  403. new_name = str(data)
  404. if old_name != new_name and new_name != '':
  405. # rename the object
  406. obj.options["name"] = deepcopy(data)
  407. self.app.object_status_changed.emit(obj, 'rename', old_name)
  408. # update the SHELL auto-completer model data
  409. try:
  410. self.app.myKeywords.remove(old_name)
  411. self.app.myKeywords.append(new_name)
  412. self.app.shell._edit.set_model_data(self.app.myKeywords)
  413. except Exception as e:
  414. log.debug(
  415. "setData() --> Could not remove the old object name from auto-completer model list. %s" %
  416. str(e))
  417. # obj.build_ui()
  418. self.app.inform.emit(_("Object renamed from <b>{old}</b> to <b>{new}</b>").format(old=old_name,
  419. new=new_name))
  420. self.dataChanged.emit(index, index)
  421. return True
  422. else:
  423. return False
  424. def supportedDropActions(self):
  425. return Qt.MoveAction
  426. def flags(self, index):
  427. default_flags = QtCore.QAbstractItemModel.flags(self, index)
  428. if not index.isValid():
  429. return Qt.ItemIsEnabled | default_flags
  430. # Prevent groups from selection
  431. try:
  432. if not index.internalPointer().obj:
  433. return Qt.ItemIsEnabled
  434. else:
  435. return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable | \
  436. Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled
  437. except AttributeError:
  438. return Qt.ItemIsEnabled
  439. # return QtWidgets.QAbstractItemModel.flags(self, index)
  440. def append(self, obj, active=False, to_index=None):
  441. log.debug(str(inspect.stack()[1][3]) + " --> OC.append()")
  442. name = obj.options["name"]
  443. # Check promises and clear if exists
  444. if name in self.promises:
  445. self.promises.remove(name)
  446. # log.debug("Promised object %s became available." % name)
  447. # log.debug("%d promised objects remaining." % len(self.promises))
  448. # Prevent same name
  449. while name in self.get_names():
  450. # ## Create a new name
  451. # Ends with number?
  452. log.debug("app_obj.new_object(): Object name (%s) exists, changing." % name)
  453. match = re.search(r'(.*[^\d])?(\d+)$', name)
  454. if match: # Yes: Increment the number!
  455. base = match.group(1) or ''
  456. num = int(match.group(2))
  457. name = base + str(num + 1)
  458. else: # No: add a number!
  459. name += "_1"
  460. obj.options["name"] = name
  461. obj.set_ui(obj.ui_type(app=self.app))
  462. # a way to signal that the object was fully loaded
  463. obj.load_complete = True
  464. # Required before appending (Qt MVC)
  465. group = self.group_items[obj.kind]
  466. group_index = self.index(group.row(), 0, QtCore.QModelIndex())
  467. if to_index is None:
  468. self.beginInsertRows(group_index, group.child_count(), group.child_count())
  469. # Append new item
  470. obj.item = TreeItem(None, self.icons[obj.kind], obj, group)
  471. # Required after appending (Qt MVC)
  472. self.endInsertRows()
  473. else:
  474. self.beginInsertRows(group_index, to_index.row()-1, to_index.row()-1)
  475. # Append new item
  476. obj.item = TreeItem(None, self.icons[obj.kind], obj, group)
  477. # Required after appending (Qt MVC)
  478. self.endInsertRows()
  479. # Expand group
  480. if group.child_count() == 1:
  481. self.view.setExpanded(group_index, True)
  482. self.app.should_we_save = True
  483. self.app.object_status_changed.emit(obj, 'append', name)
  484. # decide if to show or hide the Notebook side of the screen
  485. if self.app.defaults["global_project_autohide"] is True:
  486. # always open the notebook on object added to collection
  487. self.app.ui.splitter.setSizes([1, 1])
  488. def get_names(self):
  489. """
  490. Gets a list of the names of all objects in the collection.
  491. :return: List of names.
  492. :rtype: list
  493. """
  494. # log.debug(str(inspect.stack()[1][3]) + " --> OC.get_names()")
  495. return [x.options['name'] for x in self.get_list()]
  496. def get_bounds(self):
  497. """
  498. Finds coordinates bounding all objects in the collection.
  499. :return: [xmin, ymin, xmax, ymax]
  500. :rtype: list
  501. """
  502. log.debug(str(inspect.stack()[1][3]) + "--> OC.get_bounds()")
  503. # TODO: Move the operation out of here.
  504. xmin = inf
  505. ymin = inf
  506. xmax = -inf
  507. ymax = -inf
  508. # for obj in self.object_list:
  509. for obj in self.get_list():
  510. try:
  511. gxmin, gymin, gxmax, gymax = obj.bounds()
  512. xmin = min([xmin, gxmin])
  513. ymin = min([ymin, gymin])
  514. xmax = max([xmax, gxmax])
  515. ymax = max([ymax, gymax])
  516. except Exception as e:
  517. log.warning("DEV WARNING: Tried to get bounds of empty geometry. %s" % str(e))
  518. return [xmin, ymin, xmax, ymax]
  519. def get_by_name(self, name, isCaseSensitive=None):
  520. """
  521. Fetches the FlatCAMObj with the given `name`.
  522. :param name: The name of the object.
  523. :type name: str
  524. :param isCaseSensitive: whether searching of the object is done by name where the name is case sensitive
  525. :return: The requested object or None if no such object.
  526. :rtype: FlatCAMObj or None
  527. """
  528. # log.debug(str(inspect.stack()[1][3]) + "--> OC.get_by_name()")
  529. if isCaseSensitive is None or isCaseSensitive is True:
  530. for obj in self.get_list():
  531. if obj.options['name'] == name:
  532. return obj
  533. else:
  534. for obj in self.get_list():
  535. if obj.options['name'].lower() == name.lower():
  536. return obj
  537. return None
  538. def delete_active(self, select_project=True):
  539. selections = self.view.selectedIndexes()
  540. if len(selections) == 0:
  541. return
  542. active = selections[0].internalPointer()
  543. group = active.parent_item
  544. # send signal with the object that is deleted
  545. # self.app.object_status_changed.emit(active.obj, 'delete')
  546. # some objects add a Tab on creation, close it here
  547. for idx in range(self.app.ui.plot_tab_area.count()):
  548. if self.app.ui.plot_tab_area.widget(idx).objectName() == active.obj.options['name']:
  549. self.app.ui.plot_tab_area.removeTab(idx)
  550. break
  551. # update the SHELL auto-completer model data
  552. name = active.obj.options['name']
  553. try:
  554. self.app.myKeywords.remove(name)
  555. self.app.shell._edit.set_model_data(self.app.myKeywords)
  556. # this is not needed any more because now the code editor is created on demand
  557. # self.app.ui.code_editor.set_model_data(self.app.myKeywords)
  558. except Exception as e:
  559. log.debug(
  560. "delete_active() --> Could not remove the old object name from auto-completer model list. %s" % str(e))
  561. self.app.object_status_changed.emit(active.obj, 'delete', name)
  562. # ############ OBJECT DELETION FROM MODEL STARTS HERE ####################
  563. self.beginRemoveRows(self.index(group.row(), 0, QtCore.QModelIndex()), active.row(), active.row())
  564. group.remove_child(active)
  565. # after deletion of object store the current list of objects into the self.app.all_objects_list
  566. self.app.all_objects_list = self.get_list()
  567. self.endRemoveRows()
  568. # ############ OBJECT DELETION FROM MODEL STOPS HERE ####################
  569. if self.app.is_legacy is False:
  570. self.app.plotcanvas.redraw()
  571. if select_project:
  572. # always go to the Project Tab after object deletion as it may be done with a shortcut key
  573. self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
  574. self.app.should_we_save = True
  575. # decide if to show or hide the Notebook side of the screen
  576. if self.app.defaults["global_project_autohide"] is True:
  577. # hide the notebook if there are no objects in the collection
  578. if not self.get_list():
  579. self.app.ui.splitter.setSizes([0, 1])
  580. def delete_by_name(self, name, select_project=True):
  581. obj = self.get_by_name(name=name)
  582. item = obj.item
  583. group = self.group_items[obj.kind]
  584. group_index = self.index(group.row(), 0, QtCore.QModelIndex())
  585. item_index = self.index(item.row(), 0, group_index)
  586. deleted = item_index.internalPointer()
  587. group = deleted.parent_item
  588. # some objects add a Tab on creation, close it here
  589. for idx in range(self.app.ui.plot_tab_area.count()):
  590. if self.app.ui.plot_tab_area.widget(idx).objectName() == deleted.obj.options['name']:
  591. self.app.ui.plot_tab_area.removeTab(idx)
  592. break
  593. # update the SHELL auto-completer model data
  594. name = deleted.obj.options['name']
  595. try:
  596. self.app.myKeywords.remove(name)
  597. self.app.shell._edit.set_model_data(self.app.myKeywords)
  598. # this is not needed any more because now the code editor is created on demand
  599. # self.app.ui.code_editor.set_model_data(self.app.myKeywords)
  600. except Exception as e:
  601. log.debug(
  602. "delete_by_name() --> Could not remove the old object name from auto-completer model list. %s" % str(e))
  603. self.app.object_status_changed.emit(deleted.obj, 'delete', name)
  604. # ############ OBJECT DELETION FROM MODEL STARTS HERE ####################
  605. self.beginRemoveRows(self.index(group.row(), 0, QtCore.QModelIndex()), deleted.row(), deleted.row())
  606. group.remove_child(deleted)
  607. # after deletion of object store the current list of objects into the self.app.all_objects_list
  608. self.update_list_signal.emit()
  609. self.endRemoveRows()
  610. # ############ OBJECT DELETION FROM MODEL STOPS HERE ####################
  611. if self.app.is_legacy is False:
  612. self.app.plotcanvas.redraw()
  613. if select_project:
  614. # always go to the Project Tab after object deletion as it may be done with a shortcut key
  615. self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
  616. self.app.should_we_save = True
  617. # decide if to show or hide the Notebook side of the screen
  618. if self.app.defaults["global_project_autohide"] is True:
  619. # hide the notebook if there are no objects in the collection
  620. if not self.get_list():
  621. self.app.ui.splitter.setSizes([0, 1])
  622. def on_update_list_signal(self):
  623. self.app.all_objects_list = self.get_list()
  624. def delete_all(self):
  625. log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_all()")
  626. self.app.object_status_changed.emit(None, 'delete_all', '')
  627. try:
  628. self.app.all_objects_list.clear()
  629. self.app.geo_editor.clear()
  630. self.app.exc_editor.clear()
  631. self.app.dblsidedtool.reset_fields()
  632. self.app.panelize_tool.reset_fields()
  633. self.app.cutout_tool.reset_fields()
  634. self.app.film_tool.reset_fields()
  635. self.beginResetModel()
  636. self.checked_indexes = []
  637. for group in self.root_item.child_items:
  638. group.remove_children()
  639. self.endResetModel()
  640. self.app.plotcanvas.redraw()
  641. except Exception as e:
  642. log.debug("ObjectCollection.delete_all() --> %s" % str(e))
  643. def get_active(self):
  644. """
  645. Returns the active object or None
  646. :return: FlatCAMObj or None
  647. """
  648. selections = self.view.selectedIndexes()
  649. if len(selections) == 0:
  650. return None
  651. return selections[0].internalPointer().obj
  652. def get_selected(self):
  653. """
  654. Returns list of objects selected in the view.
  655. :return: List of objects
  656. """
  657. return [sel.internalPointer().obj for sel in self.view.selectedIndexes()]
  658. def get_non_selected(self):
  659. """
  660. Returns list of objects non-selected in the view.
  661. :return: List of objects
  662. """
  663. obj_list = self.get_list()
  664. for sel in self.get_selected():
  665. obj_list.remove(sel)
  666. return obj_list
  667. def set_active(self, name):
  668. """
  669. Selects object by name from the project list. This triggers the
  670. list_selection_changed event and call on_list_selection_changed.
  671. :param name: Name of the FlatCAM Object
  672. :return: None
  673. """
  674. try:
  675. obj = self.get_by_name(name)
  676. item = obj.item
  677. group = self.group_items[obj.kind]
  678. group_index = self.index(group.row(), 0, QtCore.QModelIndex())
  679. item_index = self.index(item.row(), 0, group_index)
  680. self.view.selectionModel().select(item_index, QtCore.QItemSelectionModel.Select)
  681. except Exception as e:
  682. log.error("[ERROR] Cause: %s" % str(e))
  683. raise
  684. def set_all_active(self):
  685. """
  686. Select all objects from the project list. This triggers the
  687. list_selection_changed event and call on_list_selection_changed.
  688. :return: None
  689. """
  690. for name in self.get_names():
  691. self.set_active(name)
  692. def set_exclusive_active(self, name):
  693. """
  694. Make the object with the name in parameters the only selected object
  695. :param name: name of object to be selected and made the only active object
  696. :return: None
  697. """
  698. self.set_all_inactive()
  699. self.set_active(name)
  700. def set_inactive(self, name):
  701. """
  702. Unselect object by name from the project list. This triggers the
  703. list_selection_changed event and call on_list_selection_changed.
  704. :param name: Name of the FlatCAM Object
  705. :return: None
  706. """
  707. # log.debug("ObjectCollection.set_inactive()")
  708. obj = self.get_by_name(name)
  709. item = obj.item
  710. group = self.group_items[obj.kind]
  711. group_index = self.index(group.row(), 0, QtCore.QModelIndex())
  712. item_index = self.index(item.row(), 0, group_index)
  713. self.view.selectionModel().select(item_index, QtCore.QItemSelectionModel.Deselect)
  714. def set_all_inactive(self):
  715. """
  716. Unselect all objects from the project list. This triggers the
  717. list_selection_changed event and call on_list_selection_changed.
  718. :return: None
  719. """
  720. for name in self.get_names():
  721. self.set_inactive(name)
  722. def on_list_selection_change(self, current, previous):
  723. """
  724. :param current: Current selected item
  725. :param previous: Previously selected item
  726. :return:
  727. """
  728. # log.debug("on_list_selection_change()")
  729. # log.debug("Current: %s, Previous %s" % (str(current), str(previous)))
  730. try:
  731. obj = current.indexes()[0].internalPointer().obj
  732. self.item_selected.emit(obj.options['name'])
  733. if obj.kind == 'gerber':
  734. self.app.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
  735. color='green',
  736. name=str(obj.options['name']),
  737. tx=_("selected"))
  738. )
  739. elif obj.kind == 'excellon':
  740. self.app.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
  741. color='brown',
  742. name=str(obj.options['name']),
  743. tx=_("selected"))
  744. )
  745. elif obj.kind == 'cncjob':
  746. self.app.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
  747. color='blue',
  748. name=str(obj.options['name']),
  749. tx=_("selected"))
  750. )
  751. elif obj.kind == 'geometry':
  752. self.app.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
  753. color='red',
  754. name=str(obj.options['name']),
  755. tx=_("selected"))
  756. )
  757. elif obj.kind == 'script':
  758. self.app.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
  759. color='orange',
  760. name=str(obj.options['name']),
  761. tx=_("selected"))
  762. )
  763. elif obj.kind == 'document':
  764. self.app.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
  765. color='darkCyan',
  766. name=str(obj.options['name']),
  767. tx=_("selected"))
  768. )
  769. except IndexError:
  770. self.item_selected.emit('none')
  771. # log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
  772. self.app.inform.emit('')
  773. try:
  774. self.app.ui.properties_scroll_area.takeWidget()
  775. except Exception as e:
  776. log.debug("Nothing to remove. %s" % str(e))
  777. self.app.setup_default_properties_tab()
  778. return
  779. if obj:
  780. obj.build_ui()
  781. def on_item_activated(self, index):
  782. """
  783. Double-click or Enter on item.
  784. :param index: Index of the item in the list.
  785. :return: None
  786. """
  787. a_idx = index.internalPointer().obj
  788. if a_idx is None:
  789. return
  790. else:
  791. try:
  792. a_idx.build_ui()
  793. except Exception as e:
  794. self.app.inform.emit('[ERROR] %s: %s' % (_("Cause of error"), str(e)))
  795. raise
  796. def get_list(self):
  797. """
  798. Will return a list of all objects currently opened. Except FlatCAMScript and FlatCAMDocuments
  799. :return:
  800. """
  801. obj_list = []
  802. for group in self.root_item.child_items:
  803. for item in group.child_items:
  804. obj_list.append(item.obj)
  805. return obj_list
  806. def update_view(self):
  807. self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
  808. def on_row_activated(self, index):
  809. if index.isValid():
  810. if index.internalPointer().parent_item != self.root_item:
  811. self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
  812. self.on_item_activated(index)
  813. def on_row_selected(self, obj_name):
  814. """
  815. This is a special string; when received it will make all Menu -> Objects entries unchecked
  816. It mean we clicked outside of the items and deselected all
  817. :param obj_name:
  818. :return:
  819. """
  820. if obj_name == 'none':
  821. for act in self.app.ui.menuobjects.actions():
  822. act.setChecked(False)
  823. return
  824. # get the name of the selected objects and add them to a list
  825. name_list = []
  826. for obj in self.get_selected():
  827. name_list.append(obj.options['name'])
  828. # set all actions as unchecked but the ones selected make them checked
  829. for act in self.app.ui.menuobjects.actions():
  830. act.setChecked(False)
  831. if act.text() in name_list:
  832. act.setChecked(True)
  833. def on_collection_updated(self, obj, state, old_name):
  834. """
  835. Create a menu from the object loaded in the collection.
  836. :param obj: object that was changed (added, deleted, renamed)
  837. :param state: what was done with the object. Can be: added, deleted, delete_all, renamed
  838. :param old_name: the old name of the object before the action that triggered this slot happened
  839. :return: None
  840. """
  841. icon_files = {
  842. "gerber": self.app.resource_location + "/flatcam_icon16.png",
  843. "excellon": self.app.resource_location + "/drill16.png",
  844. "cncjob": self.app.resource_location + "/cnc16.png",
  845. "geometry": self.app.resource_location + "/geometry16.png",
  846. "script": self.app.resource_location + "/script_new16.png",
  847. "document": self.app.resource_location + "/notes16_1.png"
  848. }
  849. if state == 'append':
  850. for act in self.app.ui.menuobjects.actions():
  851. try:
  852. act.triggered.disconnect()
  853. except TypeError:
  854. pass
  855. self.app.ui.menuobjects.clear()
  856. gerber_list = []
  857. exc_list = []
  858. cncjob_list = []
  859. geo_list = []
  860. script_list = []
  861. doc_list = []
  862. for name in self.get_names():
  863. obj_named = self.get_by_name(name)
  864. if obj_named.kind == 'gerber':
  865. gerber_list.append(name)
  866. elif obj_named.kind == 'excellon':
  867. exc_list.append(name)
  868. elif obj_named.kind == 'cncjob':
  869. cncjob_list.append(name)
  870. elif obj_named.kind == 'geometry':
  871. geo_list.append(name)
  872. elif obj_named.kind == 'script':
  873. script_list.append(name)
  874. elif obj_named.kind == 'document':
  875. doc_list.append(name)
  876. def add_act(o_name):
  877. obj_for_icon = self.get_by_name(o_name)
  878. menu_action = QtWidgets.QAction(parent=self.app.ui.menuobjects)
  879. menu_action.setCheckable(True)
  880. menu_action.setText(o_name)
  881. menu_action.setIcon(QtGui.QIcon(icon_files[obj_for_icon.kind]))
  882. menu_action.triggered.connect(
  883. lambda: self.set_active(o_name) if menu_action.isChecked() is True else
  884. self.set_inactive(o_name))
  885. self.app.ui.menuobjects.addAction(menu_action)
  886. for name in gerber_list:
  887. add_act(name)
  888. self.app.ui.menuobjects.addSeparator()
  889. for name in exc_list:
  890. add_act(name)
  891. self.app.ui.menuobjects.addSeparator()
  892. for name in cncjob_list:
  893. add_act(name)
  894. self.app.ui.menuobjects.addSeparator()
  895. for name in geo_list:
  896. add_act(name)
  897. self.app.ui.menuobjects.addSeparator()
  898. for name in script_list:
  899. add_act(name)
  900. self.app.ui.menuobjects.addSeparator()
  901. for name in doc_list:
  902. add_act(name)
  903. self.app.ui.menuobjects.addSeparator()
  904. self.app.ui.menuobjects_selall = self.app.ui.menuobjects.addAction(
  905. QtGui.QIcon(self.app.resource_location + '/select_all.png'),
  906. _('Select All')
  907. )
  908. self.app.ui.menuobjects_unselall = self.app.ui.menuobjects.addAction(
  909. QtGui.QIcon(self.app.resource_location + '/deselect_all32.png'),
  910. _('Deselect All')
  911. )
  912. self.app.ui.menuobjects_selall.triggered.connect(lambda: self.on_objects_selection(True))
  913. self.app.ui.menuobjects_unselall.triggered.connect(lambda: self.on_objects_selection(False))
  914. elif state == 'delete':
  915. for act in self.app.ui.menuobjects.actions():
  916. if act.text() == obj.options['name']:
  917. try:
  918. act.triggered.disconnect()
  919. except TypeError:
  920. pass
  921. self.app.ui.menuobjects.removeAction(act)
  922. break
  923. elif state == 'rename':
  924. for act in self.app.ui.menuobjects.actions():
  925. if act.text() == old_name:
  926. add_action = QtWidgets.QAction(parent=self.app.ui.menuobjects)
  927. add_action.setText(obj.options['name'])
  928. add_action.setIcon(QtGui.QIcon(icon_files[obj.kind]))
  929. add_action.triggered.connect(
  930. lambda: self.set_active(obj.options['name']) if add_action.isChecked() is True else
  931. self.set_inactive(obj.options['name']))
  932. self.app.ui.menuobjects.insertAction(act, add_action)
  933. try:
  934. act.triggered.disconnect()
  935. except TypeError:
  936. pass
  937. self.app.ui.menuobjects.removeAction(act)
  938. break
  939. elif state == 'delete_all':
  940. for act in self.app.ui.menuobjects.actions():
  941. try:
  942. act.triggered.disconnect()
  943. except TypeError:
  944. pass
  945. self.app.ui.menuobjects.clear()
  946. self.app.ui.menuobjects.addSeparator()
  947. self.app.ui.menuobjects_selall = self.app.ui.menuobjects.addAction(
  948. QtGui.QIcon(self.app.resource_location + '/select_all.png'),
  949. _('Select All')
  950. )
  951. self.app.ui.menuobjects_unselall = self.app.ui.menuobjects.addAction(
  952. QtGui.QIcon(self.app.resource_location + '/deselect_all32.png'),
  953. _('Deselect All')
  954. )
  955. self.app.ui.menuobjects_selall.triggered.connect(lambda: self.on_objects_selection(True))
  956. self.app.ui.menuobjects_unselall.triggered.connect(lambda: self.on_objects_selection(False))
  957. def on_objects_selection(self, on_off):
  958. obj_list = self.get_names()
  959. if on_off is True:
  960. self.set_all_active()
  961. for act in self.app.ui.menuobjects.actions():
  962. try:
  963. act.setChecked(True)
  964. except Exception:
  965. pass
  966. if obj_list:
  967. self.app.inform[str, bool].emit('[selected] %s' % _("All objects are selected."), False)
  968. else:
  969. self.set_all_inactive()
  970. for act in self.app.ui.menuobjects.actions():
  971. try:
  972. act.setChecked(False)
  973. except Exception:
  974. pass
  975. if obj_list:
  976. self.app.inform[str, bool].emit('%s' % _("Objects selection is cleared."), False)
  977. else:
  978. self.app.inform[str, bool].emit('', False)