From c85e397eca479198057ee009557c19637ef4fc2b Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Tue, 5 Nov 2019 02:36:46 +0200 Subject: [PATCH] - started to add a Tool Database --- FlatCAMApp.py | 31 +- FlatCAMCommon.py | 820 ++++++++++++++++++++++++++++++++++++++- README.md | 1 + flatcamGUI/FlatCAMGUI.py | 392 +------------------ share/database32.png | Bin 0 -> 888 bytes 5 files changed, 854 insertions(+), 390 deletions(-) create mode 100644 share/database32.png diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 28335287..ce968f96 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -48,7 +48,7 @@ from flatcamGUI.PlotCanvas import * from flatcamGUI.PlotCanvasLegacy import * from flatcamGUI.FlatCAMGUI import * -from FlatCAMCommon import LoudDict +from FlatCAMCommon import LoudDict, BookmarkManager, ToolsDB from FlatCAMPostProc import load_postprocessors from flatcamEditors.FlatCAMGeoEditor import FlatCAMGeoEditor @@ -1655,6 +1655,7 @@ class App(QtCore.QObject): self.ui.menuoptions_transform_flipx.triggered.connect(self.on_flipx) self.ui.menuoptions_transform_flipy.triggered.connect(self.on_flipy) self.ui.menuoptions_view_source.triggered.connect(self.on_view_source) + self.ui.menuoptions_tools_db.triggered.connect(self.on_tools_database) self.ui.menuviewdisableall.triggered.connect(self.disable_all_plots) self.ui.menuviewdisableother.triggered.connect(self.disable_other_plots) @@ -2263,6 +2264,12 @@ class App(QtCore.QObject): self.install_bookmarks() self.book_dialog_tab = BookmarkManager(app=self, storage=self.defaults["global_bookmarks"]) + # ################################################################################## + # ############################## Tools Database #################################### + # ################################################################################## + + self.tools_db_tab = ToolsDB(app=self) + # ### System Font Parsing ### # self.f_parse = ParseFont(self) # self.parse_system_fonts() @@ -4552,6 +4559,28 @@ class App(QtCore.QObject): msgbox.exec_() # response = msgbox.clickedButton() + def on_tools_database(self): + """ + Adds the Tools Database in a Tab in Plot Area + :return: + """ + for idx in range(self.ui.plot_tab_area.count()): + if self.ui.plot_tab_area.tabText(idx) == _("Tools Database"): + # there can be only one instance of Tools Database at one time + return + + self.tools_db_tab = ToolsDB(app=self, parent=self.ui) + + # add the tab if it was closed + self.ui.plot_tab_area.addTab(self.tools_db_tab, _("Tools Database")) + + # delete the absolute and relative position and messages in the infobar + self.ui.position_label.setText("") + self.ui.rel_position_label.setText("") + + # Switch plot_area to preferences page + self.ui.plot_tab_area.setCurrentWidget(self.tools_db_tab) + def on_file_savedefaults(self): """ Callback for menu item File->Save Defaults. Saves application default options diff --git a/FlatCAMCommon.py b/FlatCAMCommon.py index 508884cc..84319d9c 100644 --- a/FlatCAMCommon.py +++ b/FlatCAMCommon.py @@ -1,10 +1,30 @@ -# ########################################################## ## +# ########################################################## # FlatCAM: 2D Post-processing for Manufacturing # # http://flatcam.org # # Author: Juan Pablo Caram (c) # # Date: 2/5/2014 # # MIT Licence # -# ########################################################## ## +# ########################################################## + +# ########################################################## +# File Modified (major mod): Marius Adrian Stanciu # +# Date: 11/4/2019 # +# ########################################################## + +from PyQt5 import QtGui, QtCore, QtWidgets +from flatcamGUI.GUIElements import FCTable, FCEntry, FCButton, FCSpinner, FCDoubleSpinner, FCComboBox, FCCheckBox + +import sys +import webbrowser +from copy import deepcopy +from datetime import datetime +import gettext +import FlatCAMTranslation as fcTranslate +import builtins + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext class LoudDict(dict): @@ -69,3 +89,799 @@ class FCSignal: except ValueError: print('Warning: function %s not removed ' 'from signal %s' % (func, self)) + + +class BookmarkManager(QtWidgets.QWidget): + + mark_rows = QtCore.pyqtSignal() + + def __init__(self, app, storage, parent=None): + super(BookmarkManager, self).__init__(parent) + + self.app = app + + assert isinstance(storage, dict), "Storage argument is not a dictionary" + + self.bm_dict = deepcopy(storage) + + # Icon and title + # self.setWindowIcon(parent.app_icon) + # self.setWindowTitle(_("Bookmark Manager")) + # self.resize(600, 400) + + # title = QtWidgets.QLabel( + # "FlatCAM
" + # ) + # title.setOpenExternalLinks(True) + + # layouts + layout = QtWidgets.QVBoxLayout() + self.setLayout(layout) + + table_hlay = QtWidgets.QHBoxLayout() + layout.addLayout(table_hlay) + + self.table_widget = FCTable(drag_drop=True, protected_rows=[0, 1]) + self.table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + table_hlay.addWidget(self.table_widget) + + self.table_widget.setColumnCount(3) + self.table_widget.setColumnWidth(0, 20) + self.table_widget.setHorizontalHeaderLabels( + [ + '#', + _('Title'), + _('Web Link') + ] + ) + self.table_widget.horizontalHeaderItem(0).setToolTip( + _("Index.\n" + "The rows in gray color will populate the Bookmarks menu.\n" + "The number of gray colored rows is set in Preferences.")) + self.table_widget.horizontalHeaderItem(1).setToolTip( + _("Description of the link that is set as an menu action.\n" + "Try to keep it short because it is installed as a menu item.")) + self.table_widget.horizontalHeaderItem(2).setToolTip( + _("Web Link. E.g: https://your_website.org ")) + + # pal = QtGui.QPalette() + # pal.setColor(QtGui.QPalette.Background, Qt.white) + + # New Bookmark + new_vlay = QtWidgets.QVBoxLayout() + layout.addLayout(new_vlay) + + new_title_lbl = QtWidgets.QLabel('%s' % _("New Bookmark")) + new_vlay.addWidget(new_title_lbl) + + form0 = QtWidgets.QFormLayout() + new_vlay.addLayout(form0) + + title_lbl = QtWidgets.QLabel('%s:' % _("Title")) + self.title_entry = FCEntry() + form0.addRow(title_lbl, self.title_entry) + + link_lbl = QtWidgets.QLabel('%s:' % _("Web Link")) + self.link_entry = FCEntry() + self.link_entry.set_value('http://') + form0.addRow(link_lbl, self.link_entry) + + # Buttons Layout + button_hlay = QtWidgets.QHBoxLayout() + layout.addLayout(button_hlay) + + add_entry_btn = FCButton(_("Add Entry")) + remove_entry_btn = FCButton(_("Remove Entry")) + export_list_btn = FCButton(_("Export List")) + import_list_btn = FCButton(_("Import List")) + closebtn = QtWidgets.QPushButton(_("Close")) + + # button_hlay.addStretch() + button_hlay.addWidget(add_entry_btn) + button_hlay.addWidget(remove_entry_btn) + + button_hlay.addWidget(export_list_btn) + button_hlay.addWidget(import_list_btn) + # button_hlay.addWidget(closebtn) + # ############################################################################## + # ######################## SIGNALS ############################################# + # ############################################################################## + + add_entry_btn.clicked.connect(self.on_add_entry) + remove_entry_btn.clicked.connect(self.on_remove_entry) + export_list_btn.clicked.connect(self.on_export_bookmarks) + import_list_btn.clicked.connect(self.on_import_bookmarks) + self.title_entry.returnPressed.connect(self.on_add_entry) + self.link_entry.returnPressed.connect(self.on_add_entry) + # closebtn.clicked.connect(self.accept) + + self.table_widget.drag_drop_sig.connect(self.mark_table_rows_for_actions) + self.build_bm_ui() + + def build_bm_ui(self): + + self.table_widget.setRowCount(len(self.bm_dict)) + + nr_crt = 0 + sorted_bookmarks = sorted(list(self.bm_dict.items()), key=lambda x: int(x[0])) + for entry, bookmark in sorted_bookmarks: + row = nr_crt + nr_crt += 1 + + title = bookmark[0] + weblink = bookmark[1] + + id_item = QtWidgets.QTableWidgetItem('%d' % int(nr_crt)) + # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + self.table_widget.setItem(row, 0, id_item) # Tool name/id + + title_item = QtWidgets.QTableWidgetItem(title) + self.table_widget.setItem(row, 1, title_item) + + weblink_txt = QtWidgets.QTextBrowser() + weblink_txt.setOpenExternalLinks(True) + weblink_txt.setFrameStyle(QtWidgets.QFrame.NoFrame) + weblink_txt.document().setDefaultStyleSheet("a{ text-decoration: none; }") + + weblink_txt.setHtml('%s' % (weblink, weblink)) + + self.table_widget.setCellWidget(row, 2, weblink_txt) + + vertical_header = self.table_widget.verticalHeader() + vertical_header.hide() + + horizontal_header = self.table_widget.horizontalHeader() + horizontal_header.setMinimumSectionSize(10) + horizontal_header.setDefaultSectionSize(70) + horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) + horizontal_header.resizeSection(0, 20) + horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents) + horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch) + + self.mark_table_rows_for_actions() + + self.app.defaults["global_bookmarks"].clear() + for key, val in self.bm_dict.items(): + self.app.defaults["global_bookmarks"][key] = deepcopy(val) + + def on_add_entry(self, **kwargs): + """ + Add a entry in the Bookmark Table and in the menu actions + :return: None + """ + if 'title' in kwargs: + title = kwargs['title'] + else: + title = self.title_entry.get_value() + if title == '': + self.app.inform.emit(f'[ERROR_NOTCL] {_("Title entry is empty.")}') + return 'fail' + + if 'link' is kwargs: + link = kwargs['link'] + else: + link = self.link_entry.get_value() + + if link == 'http://': + self.app.inform.emit(f'[ERROR_NOTCL] {_("Web link entry is empty.")}') + return 'fail' + + # if 'http' not in link or 'https' not in link: + # link = 'http://' + link + + for bookmark in self.bm_dict.values(): + if title == bookmark[0] or link == bookmark[1]: + self.app.inform.emit(f'[ERROR_NOTCL] {_("Either the Title or the Weblink already in the table.")}') + return 'fail' + + # for some reason if the last char in the weblink is a slash it does not make the link clickable + # so I remove it + if link[-1] == '/': + link = link[:-1] + # add the new entry to storage + new_entry = len(self.bm_dict) + 1 + self.bm_dict[str(new_entry)] = [title, link] + + # add the link to the menu but only if it is within the set limit + bm_limit = int(self.app.defaults["global_bookmarks_limit"]) + if len(self.bm_dict) < bm_limit: + act = QtWidgets.QAction(parent=self.app.ui.menuhelp_bookmarks) + act.setText(title) + act.setIcon(QtGui.QIcon('share/link16.png')) + act.triggered.connect(lambda: webbrowser.open(link)) + self.app.ui.menuhelp_bookmarks.insertAction(self.app.ui.menuhelp_bookmarks_manager, act) + + self.app.inform.emit(f'[success] {_("Bookmark added.")}') + + # add the new entry to the bookmark manager table + self.build_bm_ui() + + def on_remove_entry(self): + """ + Remove an Entry in the Bookmark table and from the menu actions + :return: + """ + index_list = [] + for model_index in self.table_widget.selectionModel().selectedRows(): + index = QtCore.QPersistentModelIndex(model_index) + index_list.append(index) + title_to_remove = self.table_widget.item(model_index.row(), 1).text() + + if title_to_remove == 'FlatCAM' or title_to_remove == 'Backup Site': + self.app.inform.emit('[WARNING_NOTCL] %s.' % _("This bookmark can not be removed")) + self.build_bm_ui() + return + else: + for k, bookmark in list(self.bm_dict.items()): + if title_to_remove == bookmark[0]: + # remove from the storage + self.bm_dict.pop(k, None) + + for act in self.app.ui.menuhelp_bookmarks.actions(): + if act.text() == title_to_remove: + # disconnect the signal + try: + act.triggered.disconnect() + except TypeError: + pass + # remove the action from the menu + self.app.ui.menuhelp_bookmarks.removeAction(act) + + # house keeping: it pays to have keys increased by one + new_key = 0 + new_dict = dict() + for k, v in self.bm_dict.items(): + # we start with key 1 so we can use the len(self.bm_dict) + # when adding bookmarks (keys in bm_dict) + new_key += 1 + new_dict[str(new_key)] = v + + self.bm_dict = deepcopy(new_dict) + new_dict.clear() + + self.app.inform.emit(f'[success] {_("Bookmark removed.")}') + + # for index in index_list: + # self.table_widget.model().removeRow(index.row()) + self.build_bm_ui() + + def on_export_bookmarks(self): + self.app.report_usage("on_export_bookmarks") + self.app.log.debug("on_export_bookmarks()") + + date = str(datetime.today()).rpartition('.')[0] + date = ''.join(c for c in date if c not in ':-') + date = date.replace(' ', '_') + + filter__ = "Text File (*.TXT);;All Files (*.*)" + filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export FlatCAM Preferences"), + directory='{l_save}/FlatCAM_{n}_{date}'.format( + l_save=str(self.app.get_last_save_folder()), + n=_("Bookmarks"), + date=date), + filter=filter__) + + filename = str(filename) + + if filename == "": + self.app.inform.emit('[WARNING_NOTCL] %s' % _("FlatCAM bookmarks export cancelled.")) + return + else: + try: + f = open(filename, 'w') + f.close() + except PermissionError: + self.app.inform.emit('[WARNING] %s' % + _("Permission denied, saving not possible.\n" + "Most likely another app is holding the file open and not accessible.")) + return + except IOError: + self.app.log.debug('Creating a new bookmarks file ...') + f = open(filename, 'w') + f.close() + except: + e = sys.exc_info()[0] + self.app.log.error("Could not load defaults file.") + self.app.log.error(str(e)) + self.app.inform.emit('[ERROR_NOTCL] %s' % + _("Could not load bookmarks file.")) + return + + # Save update options + try: + with open(filename, "w") as f: + for title, link in self.bm_dict.items(): + line2write = str(title) + ':' + str(link) + '\n' + f.write(line2write) + except: + self.app.inform.emit('[ERROR_NOTCL] %s' % + _("Failed to write bookmarks to file.")) + return + self.app.inform.emit('[success] %s: %s' % + (_("Exported bookmarks to"), filename)) + + def on_import_bookmarks(self): + self.app.log.debug("on_import_bookmarks()") + + filter_ = "Text File (*.txt);;All Files (*.*)" + filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Bookmarks"), + filter=filter_) + + filename = str(filename) + + if filename == "": + self.app.inform.emit('[WARNING_NOTCL] %s' % + _("FlatCAM bookmarks import cancelled.")) + else: + try: + with open(filename) as f: + bookmarks = f.readlines() + except IOError: + self.app.log.error("Could not load bookmarks file.") + self.app.inform.emit('[ERROR_NOTCL] %s' % + _("Could not load bookmarks file.")) + return + + for line in bookmarks: + proc_line = line.replace(' ', '').partition(':') + self.on_add_entry(title=proc_line[0], link=proc_line[2]) + + self.app.inform.emit('[success] %s: %s' % + (_("Imported Bookmarks from"), filename)) + + def mark_table_rows_for_actions(self): + for row in range(self.table_widget.rowCount()): + item_to_paint = self.table_widget.item(row, 0) + if row < self.app.defaults["global_bookmarks_limit"]: + item_to_paint.setBackground(QtGui.QColor('gray')) + # item_to_paint.setForeground(QtGui.QColor('black')) + else: + item_to_paint.setBackground(QtGui.QColor('white')) + # item_to_paint.setForeground(QtGui.QColor('black')) + + def rebuild_actions(self): + # rebuild the storage to reflect the order of the lines + self.bm_dict.clear() + for row in range(self.table_widget.rowCount()): + title = self.table_widget.item(row, 1).text() + wlink = self.table_widget.cellWidget(row, 2).toPlainText() + + entry = int(row) + 1 + self.bm_dict.update( + { + str(entry): [title, wlink] + } + ) + + self.app.install_bookmarks(book_dict=self.bm_dict) + + # def accept(self): + # self.rebuild_actions() + # super().accept() + + def closeEvent(self, QCloseEvent): + self.rebuild_actions() + super().closeEvent(QCloseEvent) + + +class ToolsDB(QtWidgets.QWidget): + + mark_tools_rows = QtCore.pyqtSignal() + + def __init__(self, app, parent=None): + super(ToolsDB, self).__init__(parent) + + self.app = app + self.decimals = 4 + + # layouts + layout = QtWidgets.QVBoxLayout() + self.setLayout(layout) + + table_hlay = QtWidgets.QHBoxLayout() + layout.addLayout(table_hlay) + + self.table_widget = FCTable(drag_drop=True) + self.table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + table_hlay.addWidget(self.table_widget) + + self.table_widget.setColumnCount(21) + # self.table_widget.setColumnWidth(0, 20) + self.table_widget.setHorizontalHeaderLabels( + [ + '#', + _("Tool Diameter"), + _("Tool Type"), + _("Tool Shape"), + _("Cut Z"), + _("V-Tip Diameter"), + _("V-Tip Angle"), + _("Travel Z"), + _("Feedrate"), + _("Feedrate Z"), + _("Feedrate Rapids"), + _("Spindle Speed"), + _("Dwell"), + _("Dwelltime"), + _("MultiDepth"), + _("Postprocessor"), + _("Probe Z"), + _("Probe Feedrate"), + _("ExtraCut"), + _("Toolchange"), + _("Toolchange Z"), + _("End Z"), + ] + ) + self.table_widget.horizontalHeaderItem(0).setToolTip( + _("Index.\n" + "The rows in gray color will populate the Bookmarks menu.\n" + "The number of gray colored rows is set in Preferences.")) + + # pal = QtGui.QPalette() + # pal.setColor(QtGui.QPalette.Background, Qt.white) + + # New Bookmark + new_vlay = QtWidgets.QVBoxLayout() + layout.addLayout(new_vlay) + + new_tool_lbl = QtWidgets.QLabel('%s' % _("New Tool")) + new_vlay.addWidget(new_tool_lbl) + + form0 = QtWidgets.QFormLayout() + new_vlay.addLayout(form0) + + diameter_lbl = QtWidgets.QLabel('%s:' % _("Diameter")) + self.dia_entry = FCDoubleSpinner() + self.dia_entry.set_precision(self.decimals) + self.dia_entry.set_range(0.000001, 9999.9999) + form0.addRow(diameter_lbl, self.dia_entry) + + link_lbl = QtWidgets.QLabel('%s:' % _("Web Link")) + self.link_entry = FCEntry() + self.link_entry.set_value('http://') + form0.addRow(link_lbl, self.link_entry) + + # Buttons Layout + button_hlay = QtWidgets.QHBoxLayout() + layout.addLayout(button_hlay) + + add_entry_btn = FCButton(_("Add Tool")) + remove_entry_btn = FCButton(_("Remove Tool")) + export_list_btn = FCButton(_("Export List")) + import_list_btn = FCButton(_("Import List")) + closebtn = QtWidgets.QPushButton(_("Close")) + + # button_hlay.addStretch() + button_hlay.addWidget(add_entry_btn) + button_hlay.addWidget(remove_entry_btn) + + button_hlay.addWidget(export_list_btn) + button_hlay.addWidget(import_list_btn) + # button_hlay.addWidget(closebtn) + # ############################################################################## + # ######################## SIGNALS ############################################# + # ############################################################################## + + add_entry_btn.clicked.connect(self.on_add_entry) + remove_entry_btn.clicked.connect(self.on_remove_entry) + export_list_btn.clicked.connect(self.on_export_bookmarks) + import_list_btn.clicked.connect(self.on_import_bookmarks) + self.dia_entry.returnPressed.connect(self.on_add_entry) + self.link_entry.returnPressed.connect(self.on_add_entry) + # closebtn.clicked.connect(self.accept) + + self.bm_dict = { + 1: 'tool' + } + + self.build_bm_ui() + + def build_bm_ui(self): + + self.table_widget.setRowCount(len(self.bm_dict)) + + nr_crt = 0 + sorted_bookmarks = sorted(list(self.bm_dict.items()), key=lambda x: int(x[0])) + for k, v in sorted_bookmarks: + row = nr_crt + nr_crt += 1 + + id_item = QtWidgets.QTableWidgetItem('%d' % int(nr_crt)) + id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + self.table_widget.setItem(row, 0, id_item) # Tool name/id + + dia_item = FCDoubleSpinner() + self.table_widget.setCellWidget(row, 1, dia_item) + + tt_item = FCComboBox() + self.table_widget.setCellWidget(row, 2, tt_item) + + tshape_item = FCComboBox() + self.table_widget.setCellWidget(row, 3, tshape_item) + + cutz_item = FCDoubleSpinner() + self.table_widget.setCellWidget(row, 4, cutz_item) + + vtip_dia_item = FCDoubleSpinner() + self.table_widget.setCellWidget(row, 5, vtip_dia_item) + + vtip_angle_item = FCDoubleSpinner() + self.table_widget.setCellWidget(row, 6, vtip_angle_item) + + travelz_item = FCDoubleSpinner() + self.table_widget.setCellWidget(row, 7, travelz_item) + + fr_item = FCDoubleSpinner() + self.table_widget.setCellWidget(row, 8, fr_item) + + frz_item = FCDoubleSpinner() + self.table_widget.setCellWidget(row, 9, frz_item) + + frrapids_item = FCDoubleSpinner() + self.table_widget.setCellWidget(row, 10, frrapids_item) + + spindlespeed_item = FCDoubleSpinner() + self.table_widget.setCellWidget(row, 11, spindlespeed_item) + + dwell_item = FCCheckBox() + self.table_widget.setCellWidget(row, 12, dwell_item) + + dwelltime_item = FCDoubleSpinner() + self.table_widget.setCellWidget(row, 13, dwelltime_item) + + multidepth_item = FCCheckBox() + self.table_widget.setCellWidget(row, 14, multidepth_item) + + pp_item = FCComboBox() + self.table_widget.setCellWidget(row, 15, pp_item) + + probez_item = FCDoubleSpinner() + self.table_widget.setCellWidget(row, 16, probez_item) + + probefeedrate_item = FCDoubleSpinner() + self.table_widget.setCellWidget(row, 17, probefeedrate_item) + + ecut_item = FCCheckBox() + self.table_widget.setCellWidget(row, 18, ecut_item) + + toolchange_item = FCCheckBox() + self.table_widget.setCellWidget(row, 19, toolchange_item) + + toolchangez_item = FCDoubleSpinner() + self.table_widget.setCellWidget(row, 20, toolchangez_item) + + endz_item = FCDoubleSpinner() + self.table_widget.setCellWidget(row, 21, endz_item) + + vertical_header = self.table_widget.verticalHeader() + vertical_header.hide() + + horizontal_header = self.table_widget.horizontalHeader() + horizontal_header.setMinimumSectionSize(10) + horizontal_header.setDefaultSectionSize(70) + + self.table_widget.setSizeAdjustPolicy( + QtWidgets.QAbstractScrollArea.AdjustToContents) + for x in range(1, 21): + self.table_widget.resizeColumnsToContents() + + horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) + horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) + horizontal_header.setSectionResizeMode(13, QtWidgets.QHeaderView.Fixed) + horizontal_header.setSectionResizeMode(15, QtWidgets.QHeaderView.Fixed) + horizontal_header.setSectionResizeMode(19, QtWidgets.QHeaderView.Fixed) + horizontal_header.setSectionResizeMode(20, QtWidgets.QHeaderView.Fixed) + + horizontal_header.resizeSection(0, 20) + # horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents) + # horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch) + + def on_add_entry(self, **kwargs): + """ + Add a entry in the Bookmark Table and in the menu actions + :return: None + """ + if 'title' in kwargs: + title = kwargs['title'] + else: + title = self.title_entry.get_value() + if title == '': + self.app.inform.emit(f'[ERROR_NOTCL] {_("Title entry is empty.")}') + return 'fail' + + if 'link' is kwargs: + link = kwargs['link'] + else: + link = self.link_entry.get_value() + + if link == 'http://': + self.app.inform.emit(f'[ERROR_NOTCL] {_("Web link entry is empty.")}') + return 'fail' + + # if 'http' not in link or 'https' not in link: + # link = 'http://' + link + + for bookmark in self.bm_dict.values(): + if title == bookmark[0] or link == bookmark[1]: + self.app.inform.emit(f'[ERROR_NOTCL] {_("Either the Title or the Weblink already in the table.")}') + return 'fail' + + # for some reason if the last char in the weblink is a slash it does not make the link clickable + # so I remove it + if link[-1] == '/': + link = link[:-1] + # add the new entry to storage + new_entry = len(self.bm_dict) + 1 + self.bm_dict[str(new_entry)] = [title, link] + + # add the link to the menu but only if it is within the set limit + bm_limit = int(self.app.defaults["global_bookmarks_limit"]) + if len(self.bm_dict) < bm_limit: + act = QtWidgets.QAction(parent=self.app.ui.menuhelp_bookmarks) + act.setText(title) + act.setIcon(QtGui.QIcon('share/link16.png')) + act.triggered.connect(lambda: webbrowser.open(link)) + self.app.ui.menuhelp_bookmarks.insertAction(self.app.ui.menuhelp_bookmarks_manager, act) + + self.app.inform.emit(f'[success] {_("Bookmark added.")}') + + # add the new entry to the bookmark manager table + self.build_bm_ui() + + def on_remove_entry(self): + """ + Remove an Entry in the Bookmark table and from the menu actions + :return: + """ + index_list = [] + for model_index in self.table_widget.selectionModel().selectedRows(): + index = QtCore.QPersistentModelIndex(model_index) + index_list.append(index) + title_to_remove = self.table_widget.item(model_index.row(), 1).text() + + if title_to_remove == 'FlatCAM' or title_to_remove == 'Backup Site': + self.app.inform.emit('[WARNING_NOTCL] %s.' % _("This bookmark can not be removed")) + self.build_bm_ui() + return + else: + for k, bookmark in list(self.bm_dict.items()): + if title_to_remove == bookmark[0]: + # remove from the storage + self.bm_dict.pop(k, None) + + for act in self.app.ui.menuhelp_bookmarks.actions(): + if act.text() == title_to_remove: + # disconnect the signal + try: + act.triggered.disconnect() + except TypeError: + pass + # remove the action from the menu + self.app.ui.menuhelp_bookmarks.removeAction(act) + + # house keeping: it pays to have keys increased by one + new_key = 0 + new_dict = dict() + for k, v in self.bm_dict.items(): + # we start with key 1 so we can use the len(self.bm_dict) + # when adding bookmarks (keys in bm_dict) + new_key += 1 + new_dict[str(new_key)] = v + + self.bm_dict = deepcopy(new_dict) + new_dict.clear() + + self.app.inform.emit(f'[success] {_("Bookmark removed.")}') + + # for index in index_list: + # self.table_widget.model().removeRow(index.row()) + self.build_bm_ui() + + def on_export_bookmarks(self): + self.app.report_usage("on_export_bookmarks") + self.app.log.debug("on_export_bookmarks()") + + date = str(datetime.today()).rpartition('.')[0] + date = ''.join(c for c in date if c not in ':-') + date = date.replace(' ', '_') + + filter__ = "Text File (*.TXT);;All Files (*.*)" + filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export FlatCAM Preferences"), + directory='{l_save}/FlatCAM_{n}_{date}'.format( + l_save=str(self.app.get_last_save_folder()), + n=_("Bookmarks"), + date=date), + filter=filter__) + + filename = str(filename) + + if filename == "": + self.app.inform.emit('[WARNING_NOTCL] %s' % _("FlatCAM bookmarks export cancelled.")) + return + else: + try: + f = open(filename, 'w') + f.close() + except PermissionError: + self.app.inform.emit('[WARNING] %s' % + _("Permission denied, saving not possible.\n" + "Most likely another app is holding the file open and not accessible.")) + return + except IOError: + self.app.log.debug('Creating a new bookmarks file ...') + f = open(filename, 'w') + f.close() + except: + e = sys.exc_info()[0] + self.app.log.error("Could not load defaults file.") + self.app.log.error(str(e)) + self.app.inform.emit('[ERROR_NOTCL] %s' % + _("Could not load bookmarks file.")) + return + + # Save update options + try: + with open(filename, "w") as f: + for title, link in self.bm_dict.items(): + line2write = str(title) + ':' + str(link) + '\n' + f.write(line2write) + except: + self.app.inform.emit('[ERROR_NOTCL] %s' % + _("Failed to write bookmarks to file.")) + return + self.app.inform.emit('[success] %s: %s' % + (_("Exported bookmarks to"), filename)) + + def on_import_bookmarks(self): + self.app.log.debug("on_import_bookmarks()") + + filter_ = "Text File (*.txt);;All Files (*.*)" + filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Bookmarks"), + filter=filter_) + + filename = str(filename) + + if filename == "": + self.app.inform.emit('[WARNING_NOTCL] %s' % + _("FlatCAM bookmarks import cancelled.")) + else: + try: + with open(filename) as f: + bookmarks = f.readlines() + except IOError: + self.app.log.error("Could not load bookmarks file.") + self.app.inform.emit('[ERROR_NOTCL] %s' % + _("Could not load bookmarks file.")) + return + + for line in bookmarks: + proc_line = line.replace(' ', '').partition(':') + self.on_add_entry(title=proc_line[0], link=proc_line[2]) + + self.app.inform.emit('[success] %s: %s' % + (_("Imported Bookmarks from"), filename)) + + def rebuild_actions(self): + # rebuild the storage to reflect the order of the lines + self.bm_dict.clear() + for row in range(self.table_widget.rowCount()): + title = self.table_widget.item(row, 1).text() + wlink = self.table_widget.cellWidget(row, 2).toPlainText() + + entry = int(row) + 1 + self.bm_dict.update( + { + str(entry): [title, wlink] + } + ) + + self.app.install_bookmarks(book_dict=self.bm_dict) + + # def accept(self): + # self.rebuild_actions() + # super().accept() + + def closeEvent(self, QCloseEvent): + self.rebuild_actions() + super().closeEvent(QCloseEvent) diff --git a/README.md b/README.md index b75fd366..573b75b4 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ CAD program, and create G-Code for Isolation routing. - getting rid of all the Options GUI and related functions as it is no longer supported - updated the UI in Geometry UI - optimized the order of the defaults storage declaration and the update of the Preferences GUI from the defaults +- started to add a Tool Database 3.11.2019 diff --git a/flatcamGUI/FlatCAMGUI.py b/flatcamGUI/FlatCAMGUI.py index e4e03a8d..db0cfc81 100644 --- a/flatcamGUI/FlatCAMGUI.py +++ b/flatcamGUI/FlatCAMGUI.py @@ -337,21 +337,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.menuedit.addSeparator() self.menueditpreferences = self.menuedit.addAction(QtGui.QIcon('share/pref.png'), _('&Preferences\tSHIFT+P')) - # ## Options # ## + # ######################################################################## + # ########################## OPTIONS # ################################### + # ######################################################################## + self.menuoptions = self.menu.addMenu(_('Options')) - # self.menuoptions_transfer = self.menuoptions.addMenu(QtGui.QIcon('share/transfer.png'), 'Transfer options') - # self.menuoptions_transfer_a2p = self.menuoptions_transfer.addAction("Application to Project") - # self.menuoptions_transfer_p2a = self.menuoptions_transfer.addAction("Project to Application") - # self.menuoptions_transfer_p2o = self.menuoptions_transfer.addAction("Project to Object") - # self.menuoptions_transfer_o2p = self.menuoptions_transfer.addAction("Object to Project") - # self.menuoptions_transfer_a2o = self.menuoptions_transfer.addAction("Application to Object") - # self.menuoptions_transfer_o2a = self.menuoptions_transfer.addAction("Object to Application") - - # Separator - # self.menuoptions.addSeparator() - - # self.menuoptions_transform = self.menuoptions.addMenu(QtGui.QIcon('share/transform.png'), - # '&Transform Object') self.menuoptions_transform_rotate = self.menuoptions.addAction(QtGui.QIcon('share/rotate.png'), _("&Rotate Selection\tSHIFT+(R)")) # Separator @@ -373,6 +363,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.menuoptions_view_source = self.menuoptions.addAction(QtGui.QIcon('share/source32.png'), _("View source\tALT+S")) + self.menuoptions_tools_db = self.menuoptions.addAction(QtGui.QIcon('share/database32.png'), _("Tools DataBase")) # Separator self.menuoptions.addSeparator() @@ -3799,377 +3790,4 @@ class FlatCAMSystemTray(QtWidgets.QSystemTrayIcon): exitAction.triggered.connect(self.app.final_save) - -class BookmarkManager(QtWidgets.QWidget): - - mark_rows = QtCore.pyqtSignal() - - def __init__(self, app, storage, parent=None): - super(BookmarkManager, self).__init__(parent) - - self.app = app - - assert isinstance(storage, dict), "Storage argument is not a dictionary" - - self.bm_dict = deepcopy(storage) - - # Icon and title - # self.setWindowIcon(parent.app_icon) - # self.setWindowTitle(_("Bookmark Manager")) - # self.resize(600, 400) - - # title = QtWidgets.QLabel( - # "FlatCAM
" - # ) - # title.setOpenExternalLinks(True) - - # layouts - layout = QtWidgets.QVBoxLayout() - self.setLayout(layout) - - table_hlay = QtWidgets.QHBoxLayout() - layout.addLayout(table_hlay) - - self.table_widget = FCTable(drag_drop=True, protected_rows=[0, 1]) - self.table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) - table_hlay.addWidget(self.table_widget) - - self.table_widget.setColumnCount(3) - self.table_widget.setColumnWidth(0, 20) - self.table_widget.setHorizontalHeaderLabels( - [ - '#', - _('Title'), - _('Web Link') - ] - ) - self.table_widget.horizontalHeaderItem(0).setToolTip( - _("Index.\n" - "The rows in gray color will populate the Bookmarks menu.\n" - "The number of gray colored rows is set in Preferences.")) - self.table_widget.horizontalHeaderItem(1).setToolTip( - _("Description of the link that is set as an menu action.\n" - "Try to keep it short because it is installed as a menu item.")) - self.table_widget.horizontalHeaderItem(2).setToolTip( - _("Web Link. E.g: https://your_website.org ")) - - # pal = QtGui.QPalette() - # pal.setColor(QtGui.QPalette.Background, Qt.white) - - # New Bookmark - new_vlay = QtWidgets.QVBoxLayout() - layout.addLayout(new_vlay) - - new_title_lbl = QtWidgets.QLabel('%s' % _("New Bookmark")) - new_vlay.addWidget(new_title_lbl) - - form0 = QtWidgets.QFormLayout() - new_vlay.addLayout(form0) - - title_lbl = QtWidgets.QLabel('%s:' % _("Title")) - self.title_entry = FCEntry() - form0.addRow(title_lbl, self.title_entry) - - link_lbl = QtWidgets.QLabel('%s:' % _("Web Link")) - self.link_entry = FCEntry() - self.link_entry.set_value('http://') - form0.addRow(link_lbl, self.link_entry) - - # Buttons Layout - button_hlay = QtWidgets.QHBoxLayout() - layout.addLayout(button_hlay) - - add_entry_btn = FCButton(_("Add Entry")) - remove_entry_btn = FCButton(_("Remove Entry")) - export_list_btn = FCButton(_("Export List")) - import_list_btn = FCButton(_("Import List")) - closebtn = QtWidgets.QPushButton(_("Close")) - - # button_hlay.addStretch() - button_hlay.addWidget(add_entry_btn) - button_hlay.addWidget(remove_entry_btn) - - button_hlay.addWidget(export_list_btn) - button_hlay.addWidget(import_list_btn) - # button_hlay.addWidget(closebtn) - # ############################################################################## - # ######################## SIGNALS ############################################# - # ############################################################################## - - add_entry_btn.clicked.connect(self.on_add_entry) - remove_entry_btn.clicked.connect(self.on_remove_entry) - export_list_btn.clicked.connect(self.on_export_bookmarks) - import_list_btn.clicked.connect(self.on_import_bookmarks) - self.title_entry.returnPressed.connect(self.on_add_entry) - self.link_entry.returnPressed.connect(self.on_add_entry) - # closebtn.clicked.connect(self.accept) - - self.table_widget.drag_drop_sig.connect(self.mark_table_rows_for_actions) - self.build_bm_ui() - - def build_bm_ui(self): - - self.table_widget.setRowCount(len(self.bm_dict)) - - nr_crt = 0 - sorted_bookmarks = sorted(list(self.bm_dict.items()), key=lambda x: int(x[0])) - for entry, bookmark in sorted_bookmarks: - row = nr_crt - nr_crt += 1 - - title = bookmark[0] - weblink = bookmark[1] - - id_item = QtWidgets.QTableWidgetItem('%d' % int(nr_crt)) - # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) - self.table_widget.setItem(row, 0, id_item) # Tool name/id - - title_item = QtWidgets.QTableWidgetItem(title) - self.table_widget.setItem(row, 1, title_item) - - weblink_txt = QtWidgets.QTextBrowser() - weblink_txt.setOpenExternalLinks(True) - weblink_txt.setFrameStyle(QtWidgets.QFrame.NoFrame) - weblink_txt.document().setDefaultStyleSheet("a{ text-decoration: none; }") - - weblink_txt.setHtml('%s' % (weblink, weblink)) - - self.table_widget.setCellWidget(row, 2, weblink_txt) - - vertical_header = self.table_widget.verticalHeader() - vertical_header.hide() - - horizontal_header = self.table_widget.horizontalHeader() - horizontal_header.setMinimumSectionSize(10) - horizontal_header.setDefaultSectionSize(70) - horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) - horizontal_header.resizeSection(0, 20) - horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents) - horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch) - - self.mark_table_rows_for_actions() - - self.app.defaults["global_bookmarks"].clear() - for key, val in self.bm_dict.items(): - self.app.defaults["global_bookmarks"][key] = deepcopy(val) - - def on_add_entry(self, **kwargs): - """ - Add a entry in the Bookmark Table and in the menu actions - :return: None - """ - if 'title' in kwargs: - title = kwargs['title'] - else: - title = self.title_entry.get_value() - if title == '': - self.app.inform.emit(f'[ERROR_NOTCL] {_("Title entry is empty.")}') - return 'fail' - - if 'link' is kwargs: - link = kwargs['link'] - else: - link = self.link_entry.get_value() - - if link == 'http://': - self.app.inform.emit(f'[ERROR_NOTCL] {_("Web link entry is empty.")}') - return 'fail' - - # if 'http' not in link or 'https' not in link: - # link = 'http://' + link - - for bookmark in self.bm_dict.values(): - if title == bookmark[0] or link == bookmark[1]: - self.app.inform.emit(f'[ERROR_NOTCL] {_("Either the Title or the Weblink already in the table.")}') - return 'fail' - - # for some reason if the last char in the weblink is a slash it does not make the link clickable - # so I remove it - if link[-1] == '/': - link = link[:-1] - # add the new entry to storage - new_entry = len(self.bm_dict) + 1 - self.bm_dict[str(new_entry)] = [title, link] - - # add the link to the menu but only if it is within the set limit - bm_limit = int(self.app.defaults["global_bookmarks_limit"]) - if len(self.bm_dict) < bm_limit: - act = QtWidgets.QAction(parent=self.app.ui.menuhelp_bookmarks) - act.setText(title) - act.setIcon(QtGui.QIcon('share/link16.png')) - act.triggered.connect(lambda: webbrowser.open(link)) - self.app.ui.menuhelp_bookmarks.insertAction(self.app.ui.menuhelp_bookmarks_manager, act) - - self.app.inform.emit(f'[success] {_("Bookmark added.")}') - - # add the new entry to the bookmark manager table - self.build_bm_ui() - - def on_remove_entry(self): - """ - Remove an Entry in the Bookmark table and from the menu actions - :return: - """ - index_list = [] - for model_index in self.table_widget.selectionModel().selectedRows(): - index = QtCore.QPersistentModelIndex(model_index) - index_list.append(index) - title_to_remove = self.table_widget.item(model_index.row(), 1).text() - - if title_to_remove == 'FlatCAM' or title_to_remove == 'Backup Site': - self.app.inform.emit('[WARNING_NOTCL] %s.' % _("This bookmark can not be removed")) - self.build_bm_ui() - return - else: - for k, bookmark in list(self.bm_dict.items()): - if title_to_remove == bookmark[0]: - # remove from the storage - self.bm_dict.pop(k, None) - - for act in self.app.ui.menuhelp_bookmarks.actions(): - if act.text() == title_to_remove: - # disconnect the signal - try: - act.triggered.disconnect() - except TypeError: - pass - # remove the action from the menu - self.app.ui.menuhelp_bookmarks.removeAction(act) - - # house keeping: it pays to have keys increased by one - new_key = 0 - new_dict = dict() - for k, v in self.bm_dict.items(): - # we start with key 1 so we can use the len(self.bm_dict) - # when adding bookmarks (keys in bm_dict) - new_key += 1 - new_dict[str(new_key)] = v - - self.bm_dict = deepcopy(new_dict) - new_dict.clear() - - self.app.inform.emit(f'[success] {_("Bookmark removed.")}') - - # for index in index_list: - # self.table_widget.model().removeRow(index.row()) - self.build_bm_ui() - - def on_export_bookmarks(self): - self.app.report_usage("on_export_bookmarks") - self.app.log.debug("on_export_bookmarks()") - - date = str(datetime.today()).rpartition('.')[0] - date = ''.join(c for c in date if c not in ':-') - date = date.replace(' ', '_') - - filter__ = "Text File (*.TXT);;All Files (*.*)" - filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export FlatCAM Preferences"), - directory='{l_save}/FlatCAM_{n}_{date}'.format( - l_save=str(self.app.get_last_save_folder()), - n=_("Bookmarks"), - date=date), - filter=filter__) - - filename = str(filename) - - if filename == "": - self.app.inform.emit('[WARNING_NOTCL] %s' % _("FlatCAM bookmarks export cancelled.")) - return - else: - try: - f = open(filename, 'w') - f.close() - except PermissionError: - self.app.inform.emit('[WARNING] %s' % - _("Permission denied, saving not possible.\n" - "Most likely another app is holding the file open and not accessible.")) - return - except IOError: - self.app.log.debug('Creating a new bookmarks file ...') - f = open(filename, 'w') - f.close() - except: - e = sys.exc_info()[0] - self.app.log.error("Could not load defaults file.") - self.app.log.error(str(e)) - self.app.inform.emit('[ERROR_NOTCL] %s' % - _("Could not load bookmarks file.")) - return - - # Save update options - try: - with open(filename, "w") as f: - for title, link in self.bm_dict.items(): - line2write = str(title) + ':' + str(link) + '\n' - f.write(line2write) - except: - self.app.inform.emit('[ERROR_NOTCL] %s' % - _("Failed to write bookmarks to file.")) - return - self.app.inform.emit('[success] %s: %s' % - (_("Exported bookmarks to"), filename)) - - def on_import_bookmarks(self): - self.app.log.debug("on_import_bookmarks()") - - filter_ = "Text File (*.txt);;All Files (*.*)" - filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Bookmarks"), - filter=filter_) - - filename = str(filename) - - if filename == "": - self.app.inform.emit('[WARNING_NOTCL] %s' % - _("FlatCAM bookmarks import cancelled.")) - else: - try: - with open(filename) as f: - bookmarks = f.readlines() - except IOError: - self.app.log.error("Could not load bookmarks file.") - self.app.inform.emit('[ERROR_NOTCL] %s' % - _("Could not load bookmarks file.")) - return - - for line in bookmarks: - proc_line = line.replace(' ', '').partition(':') - self.on_add_entry(title=proc_line[0], link=proc_line[2]) - - self.app.inform.emit('[success] %s: %s' % - (_("Imported Bookmarks from"), filename)) - - def mark_table_rows_for_actions(self): - for row in range(self.table_widget.rowCount()): - item_to_paint = self.table_widget.item(row, 0) - if row < self.app.defaults["global_bookmarks_limit"]: - item_to_paint.setBackground(QtGui.QColor('gray')) - # item_to_paint.setForeground(QtGui.QColor('black')) - else: - item_to_paint.setBackground(QtGui.QColor('white')) - # item_to_paint.setForeground(QtGui.QColor('black')) - - def rebuild_actions(self): - # rebuild the storage to reflect the order of the lines - self.bm_dict.clear() - for row in range(self.table_widget.rowCount()): - title = self.table_widget.item(row, 1).text() - wlink = self.table_widget.cellWidget(row, 2).toPlainText() - - entry = int(row) + 1 - self.bm_dict.update( - { - str(entry): [title, wlink] - } - ) - - self.app.install_bookmarks(book_dict=self.bm_dict) - - # def accept(self): - # self.rebuild_actions() - # super().accept() - - def closeEvent(self, QCloseEvent): - self.rebuild_actions() - super().closeEvent(QCloseEvent) - # end of file diff --git a/share/database32.png b/share/database32.png new file mode 100644 index 0000000000000000000000000000000000000000..afa3a43485058e9fc30691765977d7dccd59cba1 GIT binary patch literal 888 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f+@+{-GzZ+Rj;xUkjGiz z5n0T@z;^_M8K-LVNdpDhOFVsD*&lPWh;#5i-RZszsL9RK#WBR=_|~i5IX#IItPk3^ zbzYdQrMWb(uv5}=tKuyM-LkxeW=fiYovo?nvB{~bW=Rh>6>)DAu+CKBb==C)>nd!# zvM)?^?W=el7u}McbMk3wk7EB?zW+XN&igs<&!4VeXH!{kZ=l<sEqWkTbnc@wdCvtVWUb&HV!}WPBPxj5^3!<}q&lfD!T_?3c zd+$cyLv3s3-qcuFs8w=(;hZzJZ{D5nU-BU7s-@tXU>ngF2JhxrM)FR#&W|}B!fK)| zP<--U$-Dcjj>JvhJ8`)s=hOdLeHO1;(sI{zSHE2AsJu@xekND9i|*C8eJkvu53^i& zJ|&pTb#2(vOIn)^^gbLkUcOgoNw=5al6fHyCb~=3{3x}0>gemI+2U9JV^OGM*kRVF zTF&JKF?-|GjWt#l2CcU`epv2^Y2^Ec+g&BqdU+>4q^u8VJ-X_uRPpIL|Nm2*miw)p z*3s3W6}p0>@j%Cp&2gQxPfR(~GGm7I&gC0ymWxbl4gRr2^Yury#V19ASaPjwuPpkr ze(}>Nn-3w;lN6pF6kh(e_=4o#U3&zKQ*N*<(=H+RqUNj%duCuoIymYTN=2v z!%OtkftJMgUtfPbQ=4D>N$Ydb-{-#=6`1B)b~yYi1ST@o64!{5l*E!$tK_0oAjM#0 zU}&jpXsByo5@KX*Wnf`tXsT^sU}a#C?5=Bpq9HdwB{QuOw+11pslZ&oAPKS|I6tkV oJh3R1p}f3YFEcN@I61K(RWH9NefB#WDWD<-Pgg&ebxsLQ0F5SWfdBvi literal 0 HcmV?d00001