class ConnectSettingsDialog(QDialog): def __init__(self, endpoints, security_mode, security_policy, certificate_path, private_key_path): super().__init__() self.ui = Ui_ConnectSettingsDialog() self.ui.setupUi(self) self.endpoints_model = QStandardItemModel() self.ui.endpointsView.setModel(self.endpoints_model) self.endpoints_model.setHorizontalHeaderLabels([ "Endpoint URL", "Security Mode", "Security Policy", "Transport Profile URI" ]) self.ui.endpointsView.setColumnWidth(0, 300) self.ui.endpointsView.selectionModel().selectionChanged.connect( self.select_security) # Dict of endpoints with security modes as keys and security policies as values self.endpoints_dict = {"None": [], "Sign": [], "SignAndEncrypt": []} self.certificate_path = certificate_path self.private_key_path = private_key_path self._init_fields(endpoints, security_mode, security_policy) self.ui.modeComboBox.currentTextChanged.connect(self._change_policies) self.ui.policyComboBox.currentTextChanged.connect( self._select_endpoint) self.ui.certificateButton.clicked.connect(self.select_certificate) self.ui.privateKeyButton.clicked.connect(self.select_private_key) self.ui.generateButton.clicked.connect(self.generate_certificate) self.ui.connectButton.clicked.connect(self.accept) self.ui.cancelButton.clicked.connect(self.reject) @trycatchslot def _init_fields(self, endpoints, security_mode, security_policy): for edp in endpoints: mode = edp.SecurityMode.name if mode == "None_": mode = "None" policy = edp.SecurityPolicyUri.split("#")[1] transport_profile = edp.TransportProfileUri row = [ QStandardItem(edp.EndpointUrl), QStandardItem(mode), QStandardItem(policy), QStandardItem(transport_profile) ] if transport_profile == "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary": # Endpoint supported self.endpoints_dict[mode].append(policy) else: # Endpoint not supported for col in row: col.setData(QBrush(QColor(255, 183, 183)), Qt.BackgroundRole) self.endpoints_model.appendRow(row) self.ui.modeComboBox.clear() self.ui.policyComboBox.clear() self.ui.modeComboBox.addItems(self.endpoints_dict.keys()) self.ui.modeComboBox.setCurrentText(security_mode) current_mode = self.ui.modeComboBox.currentText() self.ui.policyComboBox.addItems(self.endpoints_dict[current_mode]) self.ui.policyComboBox.setCurrentText(security_policy) self._select_endpoint(security_policy) self.ui.certificateLabel.setText(Path(self.certificate_path).name) self.ui.privateKeyLabel.setText(Path(self.private_key_path).name) self._toggle_security_fields(current_mode) def _change_policies(self, mode): self.ui.policyComboBox.clear() self.ui.policyComboBox.addItems(self.endpoints_dict[mode]) self._toggle_security_fields(mode) def _toggle_security_fields(self, mode): if mode == "None": self.ui.certificateButton.setEnabled(False) self.ui.certificateLabel.hide() self.ui.privateKeyButton.setEnabled(False) self.ui.privateKeyLabel.hide() self.ui.generateButton.setEnabled(False) self.ui.connectButton.setEnabled(True) else: self.ui.certificateButton.setEnabled(True) self.ui.certificateLabel.show() self.ui.privateKeyButton.setEnabled(True) self.ui.privateKeyLabel.show() self.ui.generateButton.setEnabled(True) if self.certificate_path and self.private_key_path: self.ui.connectButton.setEnabled(True) else: self.ui.connectButton.setEnabled(False) def _select_endpoint(self, policy): if policy: mode = self.ui.modeComboBox.currentText() # Get indices of supported endpoints idxlist = self.endpoints_model.match( self.endpoints_model.index(0, 3), Qt.DisplayRole, "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary", -1, Qt.MatchExactly) for idx in idxlist: if self.endpoints_model.data(idx.siblingAtColumn( 1)) == mode and self.endpoints_model.data( idx.siblingAtColumn(2)) == policy and idx.row( ) != self.ui.endpointsView.currentIndex().row(): self.ui.endpointsView.setCurrentIndex(idx) def select_security(self, selection): if isinstance(selection, QItemSelection): if not selection.indexes(): # no selection return idx = self.ui.endpointsView.currentIndex() transport_profile_uri = self.endpoints_model.data( idx.siblingAtColumn(3)) if transport_profile_uri != "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary": blocker = QSignalBlocker(self.ui.endpointsView.selectionModel()) self.ui.endpointsView.selectionModel().clearSelection() else: security_mode = self.endpoints_model.data(idx.siblingAtColumn(1)) security_policy = self.endpoints_model.data(idx.siblingAtColumn(2)) self.ui.modeComboBox.setCurrentText(security_mode) self.ui.policyComboBox.setCurrentText(security_policy) def select_certificate(self): path = QFileDialog.getOpenFileName(self, "Select certificate", self.certificate_path, "Certificate (*.der)")[0] if path: self.certificate_path = path self.ui.certificateLabel.setText(Path(path).name) if self.private_key_path: self.ui.connectButton.setEnabled(True) def select_private_key(self): path = QFileDialog.getOpenFileName(self, "Select private key", self.private_key_path, "Private key (*.pem)")[0] if path: self.private_key_path = path self.ui.privateKeyLabel.setText(Path(path).name) if self.certificate_path: self.ui.connectButton.setEnabled(True) def generate_certificate(self): private_key_path = QFileDialog.getSaveFileName( self, "Save private key file", "my_private_key.pem", "Private key (*.pem)")[0] if private_key_path: private_key_ext = Path(private_key_path).suffix if private_key_ext != ".pem": private_key_path += ".pem" path = Path(private_key_path).parent certificate_path = QFileDialog.getSaveFileName( self, "Save certificate file", str(path.joinpath("my_cert.der")), "Certificate (*.der)")[0] if certificate_path: certificate_ext = Path(certificate_path).suffix if certificate_ext != ".der": certificate_path += ".der" return_code = subprocess.call( f"""openssl req -x509 -newkey rsa:2048 \ -keyout {private_key_path} -nodes \ -outform der -out {certificate_path} \ -subj '/C=IT/ST=Catania/O=UniCT' \ -addext 'subjectAltName = URI:urn:example.org:OpcUa:python-client'""", stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if return_code == 0: QMessageBox.information( self, "Success", "Certificate generated successfully") self.certificate_path = certificate_path self.ui.certificateLabel.setText( Path(certificate_path).name) self.private_key_path = private_key_path self.ui.privateKeyLabel.setText( Path(private_key_path).name) self.ui.connectButton.setEnabled(True) else: QMessageBox.warning(self, "Error", "Unable to generate certificate") def get_selected_options(self): return self.ui.modeComboBox.currentText( ), self.ui.policyComboBox.currentText( ), self.certificate_path, self.private_key_path
class MainWindow(QMainWindow): BASE_DIR = os.path.dirname(os.path.abspath(__file__)) MAIN_UI_FILE = os.path.join(BASE_DIR, "main.ui") NEW_DISH_POPUP_UI_FILE = os.path.join(BASE_DIR, "new_dish_popup.ui") NEW_DISH_MULTI_POPUP_UI_FILE = os.path.join(BASE_DIR, "new_dish_multi_popup.ui") NEW_DISH_DATA_POPUP_UI_FILE = os.path.join(BASE_DIR, "new_dish_data_popup.ui") MODIFY_DISH_POPUP_UI_FILE = os.path.join(BASE_DIR, "modify_dish_popup.ui") DB_FILE = os.path.join(BASE_DIR, "restaurant.db") def __init__(self): super(MainWindow, self).__init__() # Initialize variable self.db_connection = None self.new_dish_popup = QWidget() self.new_dish_multi_popup = QWidget() self.new_dish_data_popup = QWidget() self.modify_dish_popup = QWidget() self.dish_table_model = QStandardItemModel(0, 6) self.dish_table_proxy = TableFilter() self.dish_data_table_model = QStandardItemModel(0, 6) self.dish_data_table_proxy = TableFilter() self.graph_chart = None self.graph_series = {} # Load UI designs uic.loadUi(self.MAIN_UI_FILE, self) uic.loadUi(self.NEW_DISH_POPUP_UI_FILE, self.new_dish_popup) uic.loadUi(self.NEW_DISH_MULTI_POPUP_UI_FILE, self.new_dish_multi_popup) uic.loadUi(self.NEW_DISH_DATA_POPUP_UI_FILE, self.new_dish_data_popup) uic.loadUi(self.MODIFY_DISH_POPUP_UI_FILE, self.modify_dish_popup) self.init_dish_table() self.init_dish_data_table() self.init_graph() # Connect to database self.init_db_connection() # MainWindow Bind action triggers self.action_new_dish.triggered.connect(self.show_new_dish_popup) self.action_new_dish_multi.triggered.connect( self.show_new_dish_multi_popup) self.action_new_data_multi.triggered.connect( lambda: self.modify_new_dish_data_popup_table(show=True)) self.tabWidget.currentChanged.connect(self.update_graph) # Dish Table filter bind self.dish_lineEdit.textChanged.connect( lambda text, col_idx=1: self.dish_table_proxy.set_col_regex_filter( col_idx, text)) self.lower_price_doubleSpinBox.valueChanged.connect( lambda value, col_idx=2: self.dish_table_proxy. set_col_number_filter(col_idx, value, -1)) self.higher_price_doubleSpinBox.valueChanged.connect( lambda value, col_idx=2: self.dish_table_proxy. set_col_number_filter(col_idx, -1, value)) self.lower_week_sell_spinBox.valueChanged.connect( lambda value, col_idx=3: self.dish_table_proxy. set_col_number_filter(col_idx, value, -1)) self.higher_week_sell_spinBox.valueChanged.connect( lambda value, col_idx=3: self.dish_table_proxy. set_col_number_filter(col_idx, -1, value)) # Dish Data Table filter bind self.lower_data_dateEdit.dateChanged.connect( lambda date, col_idx=1: self.dish_data_table_proxy. set_col_date_filter(col_idx, date, -1)) self.higher_data_dateEdit.dateChanged.connect( lambda date, col_idx=1: self.dish_data_table_proxy. set_col_date_filter(col_idx, -1, date)) self.data_lineEdit.textChanged.connect( lambda text, col_idx=2: self.dish_data_table_proxy. set_col_regex_filter(col_idx, text)) self.lower_data_doubleSpinBox.valueChanged.connect( lambda value, col_idx=3: self.dish_data_table_proxy. set_col_number_filter(col_idx, value, -1)) self.higher_data_doubleSpinBox.valueChanged.connect( lambda value, col_idx=3: self.dish_data_table_proxy. set_col_number_filter(col_idx, -1, value)) self.lower_data_spinBox.valueChanged.connect( lambda value, col_idx=4: self.dish_data_table_proxy. set_col_number_filter(col_idx, value, -1)) self.higher_data_spinBox.valueChanged.connect( lambda value, col_idx=4: self.dish_data_table_proxy. set_col_number_filter(col_idx, -1, value)) self.data_all_check_checkBox.stateChanged.connect( lambda state, col_idx=5: self.data_table_check_state( state, col_idx)) self.dish_data_table_model.itemChanged.connect(self.update_series) # Popup bind action triggers self.new_dish_popup.create_new_dish_btn.clicked.connect( self.create_new_dish) self.new_dish_multi_popup.pushButton_ok.clicked.connect( self.create_new_dish_multi) self.new_dish_data_popup.dateEdit.dateChanged.connect( self.modify_new_dish_data_popup_table) self.new_dish_data_popup.pushButton_ok.clicked.connect( self.create_new_dish_data) # Get current dishes self.load_dish_table() self.load_dish_data_table() self.new_dish_data_popup.dateEdit.setDate(QtCore.QDate.currentDate()) def init_dish_table(self): self.dish_tableView.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) # Set Header data and stretch for col, col_name in enumerate( ["ID", "菜品", "价格", "近7天总售出", "操作", "备注"]): self.dish_table_model.setHeaderData(col, Qt.Horizontal, col_name, Qt.DisplayRole) self.dish_table_proxy.setSourceModel(self.dish_table_model) self.dish_tableView.setModel(self.dish_table_proxy) self.dish_tableView.setColumnHidden(0, True) for (col, method) in [(1, "Regex"), (2, "Number"), (3, "Number"), (5, "Regex")]: self.dish_table_proxy.filter_method[col] = method def init_dish_data_table(self): self.data_tableView.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) for col, col_name in enumerate( ["Dish_ID", "日期", "菜品", "价格", "售出", "选择"]): self.dish_data_table_model.setHeaderData(col, Qt.Horizontal, col_name, Qt.DisplayRole) self.dish_data_table_proxy.setSourceModel(self.dish_data_table_model) self.data_tableView.setModel(self.dish_data_table_proxy) self.data_tableView.setColumnHidden(0, True) for (col, method) in [(1, "Date"), (2, "Regex"), (3, "Number"), (4, "Number")]: self.dish_data_table_proxy.filter_method[col] = method def init_graph(self): self.graph_chart = QChart(title="售出图") self.graph_chart.legend().setVisible(True) self.graph_chart.setAcceptHoverEvents(True) graph_view = QChartView(self.graph_chart) graph_view.setRenderHint(QPainter.Antialiasing) self.gridLayout_5.addWidget(graph_view) def init_db_connection(self): self.db_connection = sqlite3.connect(self.DB_FILE) cursor = self.db_connection.cursor() # check create table if not exist sql_create_dish_table = """ CREATE TABLE IF NOT EXISTS dish ( id integer PRIMARY KEY, name text NOT NULL, price numeric Not NULL, remarks text, UNIQUE (name, price) ); """ sql_create_dish_data_table = """ CREATE TABLE IF NOT EXISTS dish_data ( dish_id integer NOT NULL REFERENCES dish(id) ON DELETE CASCADE, date date, sell_num integer DEFAULT 0, PRIMARY KEY (dish_id, date), CONSTRAINT dish_fk FOREIGN KEY (dish_id) REFERENCES dish (id) ON DELETE CASCADE ); """ sql_trigger = """ CREATE TRIGGER IF NOT EXISTS place_holder_data AFTER INSERT ON dish BEGIN INSERT INTO dish_data (dish_id, date, sell_num) VALUES(new.id, null, 0); END; """ cursor.execute(sql_create_dish_table) cursor.execute(sql_create_dish_data_table) cursor.execute("PRAGMA FOREIGN_KEYS = on") cursor.execute(sql_trigger) cursor.close() def load_dish_table(self): today = datetime.today() sql_select_query = """ SELECT dish.id, dish.name, dish.price, COALESCE(SUM(dish_data.sell_num), 0), dish.remarks FROM dish LEFT JOIN dish_data ON dish.id = dish_data.dish_id WHERE dish_data.date IS NULL OR dish_data.date BETWEEN date('{}') and date('{}') GROUP BY dish.id ORDER BY dish.name, dish.price;""".format( (today - timedelta(days=7)).strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d")) cursor = self.db_connection.cursor() cursor.execute(sql_select_query) records = cursor.fetchall() for row_idx, record in enumerate(records): self.dish_table_model.appendRow(create_dish_table_row(*record)) cursor.close() self.dish_tableView.setItemDelegateForColumn( 4, DishTableDelegateCell(self.show_modify_dish_popup, self.delete_dish, self.dish_tableView)) def load_dish_data_table(self): sql_select_query = """ SELECT dish_data.dish_id, dish_data.date, dish.name, dish.price, dish_data.sell_num FROM dish_data LEFT JOIN dish ON dish_data.dish_id = dish.id WHERE dish_data.date IS NOT NULL ORDER BY dish_data.date DESC, dish.name, dish.price, dish_data.sell_num;""" cursor = self.db_connection.cursor() cursor.execute(sql_select_query) records = cursor.fetchall() for row_idx, record in enumerate(records): self.dish_data_table_model.appendRow( create_dish_data_table_row(*record)) cursor.close() self.lower_data_dateEdit.setDate(QDate.currentDate().addDays(-7)) self.higher_data_dateEdit.setDate(QDate.currentDate()) self.data_tableView.setItemDelegateForColumn( 5, DishDataTableDelegateCell(self.data_tableView)) def data_table_check_state(self, state, col): for row in range(self.dish_data_table_proxy.rowCount()): index = self.dish_data_table_proxy.mapToSource( self.dish_data_table_proxy.index(row, col)) if index.isValid(): self.dish_data_table_model.setData(index, str(state), Qt.DisplayRole) def show_new_dish_popup(self): # Move popup to center point = self.rect().center() global_point = self.mapToGlobal(point) self.new_dish_popup.move( global_point - QtCore.QPoint(self.new_dish_popup.width() // 2, self.new_dish_popup.height() // 2)) self.new_dish_popup.show() def show_new_dish_multi_popup(self): file_name = QFileDialog().getOpenFileName(None, "选择文件", "", self.tr("CSV文件 (*.csv)"))[0] self.new_dish_multi_popup.tableWidget.setRowCount(0) if file_name: with open(file_name, "r") as file: csv_reader = csv.reader(file, delimiter=",") for idx, row_data in enumerate(csv_reader): if len(row_data) == 2: name, price = row_data remark = "" elif len(row_data) == 3: name, price, remark = row_data else: QMessageBox.warning( self, "格式错误", self.tr('格式为"菜品 价格"或者"菜品 价格 备注"\n第{}行输入有误'.format( idx))) return self.new_dish_multi_popup.tableWidget.insertRow( self.new_dish_multi_popup.tableWidget.rowCount()) self.new_dish_multi_popup.tableWidget.setItem( idx, 0, QTableWidgetItem(name)) price_type = str_type(price) if price_type == str or (isinstance( price_type, (float, int)) and float(price) < 0): QMessageBox.warning( self, "格式错误", self.tr('第{}行价格输入有误'.format(idx + 1))) return self.new_dish_multi_popup.tableWidget.setItem( idx, 1, QTableWidgetItem("{:.2f}".format(float(price)))) self.new_dish_multi_popup.tableWidget.setItem( idx, 2, QTableWidgetItem(remark)) self.new_dish_multi_popup.show() def modify_new_dish_data_popup_table(self, *args, show=False): sql_select_query = """ SELECT id, name, price, dish_data.sell_num FROM dish LEFT JOIN dish_data ON dish.id=dish_data.dish_id WHERE dish_data.date IS NULL OR dish_data.date = date('{}') GROUP BY id, name, price ORDER BY dish.name, dish.price;""".format( self.new_dish_data_popup.dateEdit.date().toString("yyyy-MM-dd")) cursor = self.db_connection.cursor() cursor.execute(sql_select_query) records = cursor.fetchall() self.new_dish_data_popup.tableWidget.setRowCount(len(records)) self.new_dish_data_popup.tableWidget.setColumnHidden(0, True) for row_idx, record in enumerate(records): dish_id, name, price, sell_num = record self.new_dish_data_popup.tableWidget.setItem( row_idx, 0, QTableWidgetItem(str(dish_id))) self.new_dish_data_popup.tableWidget.setItem( row_idx, 1, QTableWidgetItem(name)) self.new_dish_data_popup.tableWidget.setItem( row_idx, 2, QTableWidgetItem("{:.2f}".format(price))) spin_box = QSpinBox() spin_box.setMaximum(9999) spin_box.setValue(sell_num) self.new_dish_data_popup.tableWidget.setCellWidget( row_idx, 3, spin_box) cursor.close() if show: self.new_dish_data_popup.show() def create_new_dish(self): cursor = self.db_connection.cursor() sql_insert = """ INSERT INTO dish(name, price, remarks) VALUES(?,?,?)""" dish_name = self.new_dish_popup.dish_name.text() dish_price = self.new_dish_popup.dish_price.value() dish_remark = self.new_dish_popup.dish_remark.toPlainText() try: cursor.execute(sql_insert, (dish_name, dish_price, dish_remark)) new_dish_id = cursor.lastrowid cursor.close() self.db_connection.commit() # Update dish table and dish comboBox in UI self.dish_table_model.appendRow( create_dish_table_row(new_dish_id, dish_name, dish_price, 0, dish_remark)) self.new_dish_popup.hide() except sqlite3.Error: cursor.close() QMessageBox.warning(self, "菜品价格重复", self.tr('菜品价格组合重复,请检查')) def create_new_dish_multi(self): cursor = self.db_connection.cursor() sql_insert = """ INSERT INTO dish(name, price, remarks) VALUES (?, ?, ?)""" for row in range(self.new_dish_multi_popup.tableWidget.rowCount()): dish_name = self.new_dish_multi_popup.tableWidget.item(row, 0).text() dish_price = float( self.new_dish_multi_popup.tableWidget.item(row, 1).text()) dish_remark = self.new_dish_multi_popup.tableWidget.item(row, 2).text() try: cursor.execute(sql_insert, (dish_name, dish_price, dish_remark)) new_dish_id = cursor.lastrowid self.dish_table_model.appendRow( create_dish_table_row(new_dish_id, dish_name, dish_price, 0, dish_remark)) except sqlite3.Error: cursor.close() QMessageBox.warning( self, "菜品价格重复", self.tr('前{}行已插入。\n第{}行菜品价格组合重复,请检查'.format(row, row + 1))) return cursor.close() self.db_connection.commit() self.new_dish_multi_popup.hide() def create_new_dish_data(self): current_date = self.new_dish_data_popup.dateEdit.date().toString( "yyyy-MM-dd") table_filter = TableFilter() table_filter.setSourceModel(self.dish_data_table_model) table_filter.set_col_regex_filter(1, current_date) for row in range(table_filter.rowCount()): index = table_filter.mapToSource(table_filter.index(0, 1)) if index.isValid(): self.dish_data_table_model.removeRow(index.row()) del table_filter cursor = self.db_connection.cursor() sql_insert = """ INSERT OR REPLACE INTO dish_data(dish_id, date, sell_num) VALUES (?, ?, ?)""" for row in range(self.new_dish_data_popup.tableWidget.rowCount()): dish_id = int( self.new_dish_data_popup.tableWidget.item(row, 0).text()) name = self.new_dish_data_popup.tableWidget.item(row, 1).text() price = float( self.new_dish_data_popup.tableWidget.item(row, 2).text()) sell_num = self.new_dish_data_popup.tableWidget.cellWidget( row, 3).value() cursor.execute(sql_insert, (dish_id, current_date, sell_num)) self.dish_data_table_model.appendRow( create_dish_data_table_row(dish_id, current_date, name, price, sell_num)) cursor.close() self.db_connection.commit() self.new_dish_data_popup.hide() def delete_dish(self, dish_id): cursor = self.db_connection.cursor() sql_delete = """ DELETE FROM dish WHERE id=?""" cursor.execute(sql_delete, tuple([dish_id])) cursor.close() self.db_connection.commit() # Update dish table and dish comboBox in UI for row in self.dish_data_table_model.findItems(str(dish_id)): index = row.index() if index.isValid(): self.dish_data_table_model.removeRow(index.row()) for row in self.dish_table_model.findItems(str(dish_id)): index = row.index() if index.isValid(): self.dish_table_model.removeRow(index.row()) def show_modify_dish_popup(self, dish_id): point = self.rect().center() global_point = self.mapToGlobal(point) self.modify_dish_popup.move( global_point - QtCore.QPoint(self.modify_dish_popup.width() // 2, self.modify_dish_popup.height() // 2)) # Find the row and get necessary info index = self.dish_table_model.match(self.dish_table_model.index(0, 0), Qt.DisplayRole, str(dish_id)) if index: row_idx = index[0] dish_name = self.dish_table_model.data(row_idx.siblingAtColumn(1)) dish_price = self.dish_table_model.data(row_idx.siblingAtColumn(2)) dish_remark = self.dish_table_model.data( row_idx.siblingAtColumn(5)) self.modify_dish_popup.dish_name.setText(dish_name) self.modify_dish_popup.dish_price.setValue(float(dish_price)) self.modify_dish_popup.dish_remark.setText(dish_remark) try: self.modify_dish_popup.modify_dish_btn.clicked.disconnect() except TypeError: pass self.modify_dish_popup.modify_dish_btn.clicked.connect( lambda: self.modify_dish(row_idx, dish_id)) self.modify_dish_popup.show() def modify_dish(self, row, dish_id): cursor = self.db_connection.cursor() sql_update = """ UPDATE dish SET name = ?, price = ?, remarks = ? WHERE id=?""" dish_name = self.modify_dish_popup.dish_name.text() dish_price = self.modify_dish_popup.dish_price.value() dish_remark = self.modify_dish_popup.dish_remark.toPlainText() cursor.execute(sql_update, (dish_name, dish_price, dish_remark, dish_id)) cursor.close() self.db_connection.commit() self.modify_dish_popup.hide() # Update dish table and dish comboBox in UI old_name = self.dish_table_model.data(row.siblingAtColumn(1)) old_price = self.dish_table_model.data(row.siblingAtColumn(2)) sell_num = self.dish_table_model.data(row.siblingAtColumn(3)) row_idx = row.row() self.dish_table_model.removeRow(row_idx) self.dish_table_model.insertRow( row_idx, create_dish_table_row(dish_id, dish_name, dish_price, sell_num, dish_remark)) for row in self.dish_data_table_model.findItems(str(dish_id)): index = row.index() if index.isValid(): self.dish_data_table_model.setData(index.siblingAtColumn(2), dish_name) self.dish_data_table_model.setData(index.siblingAtColumn(3), "{:.2f}".format(dish_price)) old_key = old_name + '(' + old_price + ')' if old_key in self.graph_line_series: self.graph_line_series[dish_name + '(' + str(dish_price) + ')'] = self.graph_line_series[old_key] del self.graph_line_series[old_key] def update_series(self, item: QStandardItem): if item.column() == 5: # check for checkbox column item_idx = item.index() date = self.dish_data_table_model.data(item_idx.siblingAtColumn(1)) dish_name = self.dish_data_table_model.data( item_idx.siblingAtColumn(2)) dish_price = self.dish_data_table_model.data( item_idx.siblingAtColumn(3)) sell_num = self.dish_data_table_model.data( item_idx.siblingAtColumn(4)) set_name = dish_name + "(" + dish_price + ")" key = str( QDateTime(QDate.fromString(date, "yyyy-MM-dd")).toSecsSinceEpoch()) if key not in self.graph_series: self.graph_series[key] = {} if int(item.text()) == 0: if set_name in self.graph_series[key]: del self.graph_series[key][set_name] if not self.graph_series[key]: del self.graph_series[key] else: self.graph_series[key][set_name] = int(sell_num) def update_graph(self, index): if index == 2: self.graph_chart.removeAllSeries() axis_x = QBarCategoryAxis() axis_x.setTitleText("日期") if self.graph_chart.axisX(): self.graph_chart.removeAxis(self.graph_chart.axisX()) self.graph_chart.addAxis(axis_x, Qt.AlignBottom) axis_y = QValueAxis() axis_y.setLabelFormat("%i") axis_y.setTitleText("售出量") if self.graph_chart.axisY(): self.graph_chart.removeAxis(self.graph_chart.axisY()) self.graph_chart.addAxis(axis_y, Qt.AlignLeft) max_num = 0 total_date = 0 set_dict = {} for key, data in sorted(self.graph_series.items(), key=lambda i: int(i[0])): axis_x.append( QDateTime.fromSecsSinceEpoch( int(key)).toString("yyyy年MM月dd日")) for set_name, value in data.items(): if set_name not in set_dict: set_dict[set_name] = QBarSet(set_name) for _ in range(total_date): set_dict[set_name].append(0) set_dict[set_name].append(value) max_num = max(max_num, value) total_date += 1 for _, bar_set in set_dict.items(): if bar_set.count() < total_date: bar_set.append(0) bar_series = QBarSeries() for _, bar_set in set_dict.items(): bar_series.append(bar_set) bar_series.hovered.connect(self.graph_tooltip) axis_y.setMax(max_num + 1) axis_y.setMin(0) self.graph_chart.addSeries(bar_series) bar_series.attachAxis(axis_x) bar_series.attachAxis(axis_y) def graph_tooltip(self, status, index, bar_set: QBarSet): if status: QToolTip.showText( QCursor.pos(), "{}\n日期: {}\n售出: {}".format(bar_set.label(), self.graph_chart.axisX().at(index), int(bar_set.at(index))))
class CommentsTable(QTableView): """ The comment table below the video. """ state_changed = pyqtSignal(bool) def __init__(self, main_handler: MainHandler): super().__init__() self.__widget_mpv = main_handler.widget_mpv self.__mpv_player = self.__widget_mpv.player palette = self.palette() palette.setColor(QPalette.Inactive, QPalette.Highlight, palette.color(QPalette.Active, QPalette.Highlight)) palette.setColor(QPalette.Inactive, QPalette.HighlightedText, palette.color(QPalette.Active, QPalette.HighlightedText)) self.setPalette(palette) # Model self.__model = QStandardItemModel(self) self.setModel(self.__model) self.selectionModel().selectionChanged.connect(lambda sel, __: self.__on_row_selection_changed()) # Headers self.horizontalHeader().setStretchLastSection(True) self.horizontalHeader().hide() self.verticalHeader().hide() # Delegates delegate_time = CommentTimeDelegate(self) delegate_coty = CommentTypeDelegate(self) delegate_note = CommentNoteDelegate(self) delegate_time.editing_done.connect(self.__on_after_user_changed_time) delegate_coty.editing_done.connect(self.__on_after_user_changed_comment_type) delegate_note.editing_done.connect(self.__on_after_user_changed_comment_note) self.setItemDelegateForColumn(0, delegate_time) self.setItemDelegateForColumn(1, delegate_coty) self.setItemDelegateForColumn(2, delegate_note) # Misc self.setEditTriggers(QAbstractItemView.DoubleClicked) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.__selection_flags = QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows self.setAlternatingRowColors(True) self.setSortingEnabled(True) self.setWordWrap(False) self.setShowGrid(False) def delete_current_selected_comment(self) -> None: """ Will delete the current selected comment row. If selection is empty, no action will be invoked. """ def delete(selected: List[QModelIndex]): self.__model.removeRows(selected[0].row(), 1) self.state_changed.emit(False) EventDistributor.send_event(EventCommentAmountChanged(self.__model.rowCount())) self.__do_with_selected_comment_row(delete) def edit_current_selected_comment(self) -> None: """ Will start edit mode on selected comment row. If selection is empty, no action will be invoked. """ def edit(selected: List[QModelIndex]): row = selected[0].row() idx = self.__model.item(row, 2).index() self.edit(idx) self.state_changed.emit(False) self.__do_with_selected_comment_row(edit) def copy_current_selected_comment(self) -> None: """ Will copy the complete row to clipboard. If selection is empty, no action will be invoked. """ def copy(selected: List[QModelIndex]): row = selected[0].row() time = self.__model.item(row, 0).text() coty = self.__model.item(row, 1).text() note = self.__model.item(row, 2).text() QApplication.clipboard().setText("[{}] [{}] {}".format(time, coty, note)) self.__do_with_selected_comment_row(copy) def add_comments(self, comments: Tuple[Comment], changes_qc=False, edit=False, resize_ct_column=True): if not comments: return model = self.__model last_entry = None # Always resize on the first comment imported resize_ct_column = resize_ct_column or not model.hasChildren() for comment in comments: time = QStandardItem(comment.comment_time) time.setTextAlignment(Qt.AlignCenter) ct = QStandardItem(_translate("CommentTypes", comment.comment_type)) note = QStandardItem(comment.comment_note) last_entry = [time, ct, note] model.appendRow(last_entry) if resize_ct_column: self.resize_column_type_column() self.sort() EventDistributor.send_event(EventCommentAmountChanged(model.rowCount())) self.__on_row_selection_changed() if changes_qc: self.state_changed.emit(False) if edit: new_index = model.indexFromItem(last_entry[2]) self.scrollTo(new_index) self.setCurrentIndex(new_index) self.edit(new_index) else: self.ensure_selection() def add_comment(self, comment_type: str) -> None: comment = Comment( comment_time=self.__widget_mpv.player.position_current(), comment_type=comment_type, comment_note="" ) self.add_comments((comment,), changes_qc=True, edit=True, resize_ct_column=False) def get_all_comments(self) -> Tuple[Comment]: """ Returns all comments. :return: all comments. """ ret_list = [] model = self.__model for r in range(0, model.rowCount()): time = model.item(r, 0).text() coty = model.item(r, 1).text() note = model.item(r, 2).text() ret_list.append(Comment(comment_time=time, comment_type=coty, comment_note=note)) return tuple(ret_list) def reset_comments_table(self) -> None: """ Will clear all comments. """ self.__model.clear() self.state_changed.emit(True) EventDistributor.send_event(EventCommentAmountChanged(self.__model.rowCount())) EventDistributor.send_event(EventCommentCurrentSelectionChanged(-1)) def sort(self) -> None: """ Will sort the comments table by time column. """ # Sorting is only triggered if the sorting policy changes self.setSortingEnabled(False) self.setSortingEnabled(True) self.sortByColumn(0, Qt.AscendingOrder) def __do_with_selected_comment_row(self, consume_selected_function) -> None: """ This function takes a **function** as argument. *It will call the function with the current selection as argument if selection is not empty.* :param consume_selected_function: The function to apply if selection is not empty """ is_empty: bool = self.__model.rowCount() == 0 if not is_empty: selected = self.selectionModel().selectedRows() if selected: consume_selected_function(selected) def __on_after_user_changed_time(self) -> None: """ Action to invoke after time was changed manually by the user. """ self.sort() self.state_changed.emit(False) def __on_after_user_changed_comment_type(self) -> None: """ Action to invoke after comment type was changed manually by the user. """ self.state_changed.emit(False) # noinspection PyMethodMayBeStatic def __on_after_user_changed_comment_note(self) -> None: """ Action to invoke after comment note was changed manually by the user. """ self.state_changed.emit(False) # noinspection PyMethodMayBeStatic def __on_row_selection_changed(self) -> None: def after_model_updated(): current_index = self.selectionModel().currentIndex() if current_index.isValid(): new_row = current_index.row() else: new_row = -1 EventDistributor.send_event(EventCommentCurrentSelectionChanged(new_row), EventReceiver.WIDGET_STATUS_BAR) QTimer.singleShot(0, after_model_updated) def keyPressEvent(self, e: QKeyEvent): mod = e.modifiers() key = e.key() # Only key up and key down are handled here because they require to call super if (key == Qt.Key_Up or key == Qt.Key_Down) and mod == Qt.NoModifier: super().keyPressEvent(e) else: self.__widget_mpv.keyPressEvent(e) def mousePressEvent(self, e: QMouseEvent): if e.button() == Qt.LeftButton: mdi: QModelIndex = self.indexAt(e.pos()) if mdi.column() == 0 and self.__mpv_player.has_video(): position = self.__model.item(mdi.row(), 0).text() self.__widget_mpv.player.position_jump(position=position) e.accept() elif mdi.column() == 1 and mdi == self.selectionModel().currentIndex(): self.edit(mdi) e.accept() super().mousePressEvent(e) def wheelEvent(self, e: QWheelEvent): delta = e.angleDelta() x_d = delta.x() y_d = delta.y() if x_d == 0 and y_d != 0: position = self.verticalScrollBar().value() if y_d > 0: self.verticalScrollBar().setValue(position - 1) else: self.verticalScrollBar().setValue(position + 1) else: super().wheelEvent(e) def ensure_selection(self) -> None: """ If no row is highlighted the first row will be highlighted. """ self.setFocus() if self.__model.rowCount() != 0: if not self.selectionModel().currentIndex().isValid(): self.__highlight_row(self.model().index(0, 2)) def perform_search(self, query: str, top_down: bool, new_query: bool, last_index: QModelIndex) -> SearchResult: """ Will perform the search for the given query and return a SearchResult. :param last_index: The index of the latest search result or any invalid index. :param query: search string ignore case (Qt.MatchContains) :param top_down: If True the next, if False the previous occurrence will be returned :param new_query: If True the search will be handled as a new one. :return: """ current_index = self.selectionModel().currentIndex() if new_query: start_row = 0 elif last_index and last_index.isValid(): start_row = last_index.row() elif current_index and current_index.isValid(): start_row = current_index.row() else: start_row = 0 if query == "": return self.__generate_search_result(query) start = self.__model.index(start_row, 2) match: List[QModelIndex] = self.__model.match(start, Qt.DisplayRole, query, -1, Qt.MatchContains | Qt.MatchWrap) if not match: return self.__generate_search_result(query) return self.__provide_search_result(query, match, top_down, new_query) def __provide_search_result(self, query: str, match: List[QModelIndex], top_down: bool, new_query: bool) -> SearchResult: if top_down and len(match) > 1: if new_query or self.selectionModel().currentIndex() not in match: model_index = match[0] else: model_index = match[1] else: model_index = match[-1] current_hit = sorted(match, key=lambda k: k.row()).index(model_index) return self.__generate_search_result(query, model_index, current_hit + 1, len(match)) def __generate_search_result(self, query, model_index=None, current_hit=0, total_hits=0) -> SearchResult: result = SearchResult(query, model_index, current_hit, total_hits) result.highlight.connect(lambda index: self.__highlight_row(index)) return result def __highlight_row(self, model_index: QModelIndex): if model_index: self.selectionModel().setCurrentIndex(model_index, self.__selection_flags) self.selectionModel().select(model_index, self.__selection_flags) self.scrollTo(model_index, QAbstractItemView.PositionAtCenter) def resize_column_type_column(self): self.resizeColumnToContents(1)
class PinStatusWidget(GalacteekTab): COL_TS = 0 COL_QUEUE = 1 COL_PATH = 2 COL_STATUS = 3 COL_PROGRESS = 4 COL_CTRL = 5 def __init__(self, gWindow, **kw): super(PinStatusWidget, self).__init__(gWindow, **kw) self.tree = QTreeView() self.tree.setObjectName('pinStatusWidget') self.boxLayout = QVBoxLayout() self.boxLayout.addWidget(self.tree) self.ctrlLayout = QHBoxLayout() self.btnPin = QPushButton(iPin()) self.pathLabel = QLabel(iCidOrPath()) self.pathEdit = QLineEdit() self.ctrlLayout.addWidget(self.pathLabel) self.ctrlLayout.addWidget(self.pathEdit) self.ctrlLayout.addWidget(self.btnPin) self.vLayout.addLayout(self.ctrlLayout) self.vLayout.addLayout(self.boxLayout) self.app.ipfsCtx.pinItemStatusChanged.connect(self.onPinStatusChanged) self.app.ipfsCtx.pinFinished.connect(self.onPinFinished) self.app.ipfsCtx.pinItemRemoved.connect(self.onItemRemoved) self.pathEdit.returnPressed.connect(self.onPathEntered) self.btnPin.clicked.connect(self.onPathEntered) self.model = QStandardItemModel() self.model.setHorizontalHeaderLabels( ['TS', iQueue(), iPath(), iStatus(), iNodesProcessed(), '']) self.tree.setSortingEnabled(True) self.tree.setModel(self.model) self.tree.sortByColumn(self.COL_TS, Qt.DescendingOrder) for col in [self.COL_QUEUE, self.COL_PATH, self.COL_PROGRESS]: self.tree.header().setSectionResizeMode( col, QHeaderView.ResizeToContents) self.tree.hideColumn(self.COL_TS) def resort(self): self.model.sort(self.COL_TS, Qt.DescendingOrder) def onPathEntered(self): text = self.pathEdit.text() self.pathEdit.clear() path = IPFSPath(text) if path.valid: ensure(self.app.ipfsCtx.pinner.queue(path.objPath, True, None)) else: messageBox(iInvalidInput()) def removeItem(self, path): modelSearch(self.model, search=path, columns=[self.COL_PATH], delete=True) def onItemRemoved(self, qname, path): self.removeItem(path) def getIndexFromPath(self, path): idxList = self.model.match(self.model.index(0, self.COL_PATH), PinObjectPathRole, path, 1, Qt.MatchFixedString | Qt.MatchWrap) if len(idxList) > 0: return idxList.pop() def findPinItems(self, path): idx = self.getIndexFromPath(path) if not idx: return None itemP = self.model.itemFromIndex(idx) if not itemP: return None idxQueue = self.model.index(itemP.row(), self.COL_QUEUE, itemP.index().parent()) idxProgress = self.model.index(itemP.row(), self.COL_PROGRESS, itemP.index().parent()) idxStatus = self.model.index(itemP.row(), self.COL_STATUS, itemP.index().parent()) idxC = self.model.index(itemP.row(), self.COL_CTRL, itemP.index().parent()) cancelButton = self.tree.indexWidget(idxC) return { 'itemPath': itemP, 'itemQname': self.model.itemFromIndex(idxQueue), 'itemProgress': self.model.itemFromIndex(idxProgress), 'itemStatus': self.model.itemFromIndex(idxStatus), 'cancelButton': cancelButton } def updatePinStatus(self, path, status, progress): idx = self.getIndexFromPath(path) if not idx: return try: itemPath = self.model.itemFromIndex(idx) if itemPath and time.time() - itemPath.lastProgressUpdate < 5: return itemProgress = self.model.itemFromIndex( self.model.index(idx.row(), self.COL_PROGRESS, idx.parent())) itemStatus = self.model.itemFromIndex( self.model.index(idx.row(), self.COL_STATUS, idx.parent())) itemStatus.setText(status) itemProgress.setText(progress) itemPath.lastProgressUpdate = time.time() except: pass def onPinFinished(self, path): items = self.findPinItems(path) if items: items['itemStatus'].setText(iPinned()) items['itemProgress'].setText('OK') color1 = QBrush(QColor('#4a9ea1')) color2 = QBrush(QColor('#66a56e')) for item in [ items['itemQname'], items['itemPath'], items['itemStatus'], items['itemProgress'] ]: item.setBackground(color1) for item in [items['itemStatus'], items['itemProgress']]: item.setBackground(color2) if items['cancelButton']: items['cancelButton'].setEnabled(False) self.resort() self.purgeFinishedItems() def purgeFinishedItems(self): maxFinished = 16 ret = modelSearch(self.model, search=iPinned(), columns=[self.COL_STATUS]) if len(ret) > maxFinished: rows = [] for idx in ret: item = self.model.itemFromIndex(idx) if not item: continue rows.append(item.row()) try: for row in list(sorted(rows))[int(maxFinished / 2):]: self.model.removeRow(row) except: pass async def onCancel(self, qname, path, *a): self.removeItem(path) await self.app.ipfsCtx.pinner.cancel(qname, path) def onPinStatusChanged(self, qname, path, statusInfo): nodesProcessed = statusInfo['status'].get('Progress', iUnknown()) idx = self.getIndexFromPath(path) if not idx: # Register it btnCancel = QToolButton() btnCancel.setIcon(getIcon('cancel.png')) btnCancel.setText(iCancel()) btnCancel.clicked.connect(partialEnsure(self.onCancel, qname, path)) btnCancel.setFixedWidth(140) displayPath = path if len(displayPath) > 64: displayPath = displayPath[0:64] + ' ..' itemTs = UneditableItem(str(statusInfo['ts_queued'])) itemQ = UneditableItem(qname) itemP = UneditableItem(displayPath) itemP.setData(path, PinObjectPathRole) itemP.setToolTip(path) itemP.lastProgressUpdate = time.time() itemStatus = UneditableItem(iPinning()) itemProgress = UneditableItem(str(nodesProcessed)) itemC = UneditableItem('') self.model.invisibleRootItem().appendRow( [itemTs, itemQ, itemP, itemStatus, itemProgress, itemC]) idx = self.model.indexFromItem(itemC) self.tree.setIndexWidget(idx, btnCancel) self.resort() else: self.updatePinStatus(path, iPinning(), str(nodesProcessed))