class RecipeWindow(QWidget): def __init__(self): super().__init__() self.recipe_title = "" self.conn = QSqlDatabase.addDatabase("QSQLITE") self.conn.setDatabaseName("recipes.db") self.initUI() def initUI(self): """ PURPOSE ----- To pass create all QWidgets to be displayed on our window. All class instances are changed dynamically, i.e. title, author, etc. All non-class instances are static widgets that will not be moved OUTPUT ----- Nothing """ self.setWindowTitle("Recipe Application") self.title = QLabel(self) self.title.setWordWrap(True) # Enables word wrap for specific label self.title.setFont(QFont('Arial', 20, QFont.Bold)) self.title.setAlignment(Qt.AlignCenter) self.title.setFixedWidth(600) self.title.move(150, 10) author_label = QLabel("Author: ", self) author_label.move(45, 120) author_label.setFont(QFont('Arial', 10, QFont.Bold)) self.author = QLabel(self) self.author.move(95, 120) self.author.setFixedWidth(165) self.author.setFont(QFont('Arial', 10)) cuisine_label = QLabel("Cuisine: ", self) cuisine_label.move(265, 120) cuisine_label.setFont(QFont('Arial', 10, QFont.Bold)) self.cuisine = QLabel(self) self.cuisine.move(320, 120) self.cuisine.setFixedWidth(160) self.cuisine.setFont(QFont('Arial', 10)) tag_label = QLabel("Tags: ", self) tag_label.move(485, 120) tag_label.setFont(QFont('Arial', 10, QFont.Bold)) self.tags = QLabel(self) self.tags.setFixedWidth(330) self.tags.setWordWrap(True) self.tags.move(525, 115) self.tags.setFont(QFont('Arial', 10)) ingredients_label = QLabel("Ingredients: ", self) ingredients_label.setFont(QFont('Arial', 10, QFont.Bold)) ingredients_label.move(45, 170) self.ingredients = QTextEdit(self) self.ingredients.setReadOnly(True) self.ingredients.setFixedWidth(200) self.ingredients.setFixedHeight(300) self.ingredients.move(30, 200) self.ingredients.setFont(QFont('Arial', 10)) recipe_label = QLabel("Recipe Procedure: ", self) recipe_label.setFont(QFont('Arial', 10, QFont.Bold)) recipe_label.move(265, 170) self.recipe = QTextEdit(self) self.recipe.setReadOnly(True) self.recipe.setFixedWidth(610) self.recipe.setFixedHeight(300) self.recipe.move(250, 200) self.recipe.setFont(QFont('Arial', 10)) website_url_label = QLabel("If you'd like to see full details of this recipe click here: ", self) website_url_label.setFont(QFont('Arial', 10, QFont.Bold)) website_url_label.move(35, 540) self.website_url = QLabel(self) self.website_url.setOpenExternalLinks(True) # Allows us to click on hyperlink self.website_url.setFixedWidth(500) self.website_url.setWordWrap(True) # Set word wrap self.website_url.move(385, 540) self.website_url.setFont(QFont('Arial', 10)) self.title_view_label = QLabel("Below are potentially some related recipes! :", self) self.title_view_label.setFont(QFont('Arial', 10)) def obtain_recipe(self): """ PURPOSE ----- To obtain all information pertaining to the recipe clicked on by user, i.e. title, author, cuisine, etc. and create a model to store that information OUTPUT ----- The model with the stored information """ recipe_info_cmd = \ f""" SELECT R.`Recipe Title`, R.Author, R.Cuisine, R.Ingredients, R.`Recipe Procedure`, R.Tags, R.'Website URL' FROM recipes R WHERE R.`Recipe Title` = "{self.recipe_title}" """ # Query to get title, author, ingredients, and recipe procedure from desired recipe title recipe_query = QSqlQuery(self.conn) # Same process as the title_query recipe_query.prepare(recipe_info_cmd) recipe_query.exec_() recipe_model = QSqlQueryModel() recipe_model.setQuery(recipe_query) return recipe_model def view_recipe(self, model): """ PURPOSE ----- To obtain all information pertaining to the recipe clicked on by user, i.e. title, author, cuisine, etc. from the model parameter and set the information to our widgets create in the initUI() function declared previously Then takes the tags from said recipe to view other recipes with one of its tags in view_related_recipes() INPUT ----- model: QSqlQueryModel() containing recipe information OUTPUT ----- Calls view_related_recipes() with array of tag values """ # All values taken from QSqlQuery returned in obtain_recipe() function recipe_title = model.record(0).value(0) # Recipe title recipe_author = model.record(0).value(1) # Recipe author recipe_cuisine = model.record(0).value(2) # Recipe cuisine recipe_ingredients = model.record(0).value(3) # Recipe ingredients recipe_steps = model.record(0).value(4) # Recipe steps recipe_tags = model.record(0).value(5) # Recipe tags recipe_website = model.record(0).value(6) # Recipe website self.setGeometry(0, 0, 900, 780) # Set our window size self.setFixedSize(self.width(), self.height()) # Everything done below is pretty self explanatory; create label/textedit, populate, move, etc. self.title.setText(recipe_title) self.title.repaint() self.author.setText(recipe_author) self.author.repaint() self.cuisine.setText(recipe_cuisine) self.cuisine.repaint() self.tags.setText(recipe_tags) self.tags.repaint() self.ingredients.setText(recipe_ingredients) self.ingredients.repaint() self.recipe.setText(recipe_steps) self.recipe.repaint() self.website_url.setText('<a href="' + recipe_website + '/">' + recipe_title + '</a>') # Creates hyperlink self.website_url.repaint() self.view_related_recipes(recipe_tags.split(",")) def view_related_recipes(self, tag_list): """ PURPOSE ----- To create a QTableView() with all recipes with like-tags of the already shown recipe INPUT ----- tag_list: list containing tag information OUTPUT ----- Displays full window containing recipe information + related recipes """ recipe_query = "" # Empty string to construct our query for elements in tag_list: # Here we construct our query through a for-loop # There shouldn't be many elements in our array in the first place, so no worries of efficiency recipe_query += f"Tags LIKE '%" + elements + "%' OR " recipe_query = recipe_query[:len(recipe_query) - 4] # to remove last OR + extra space cmd = \ f""" SELECT `Recipe Title` FROM recipes R WHERE {recipe_query} """ title_query = QSqlQuery(self.conn) # Establish our query, prepare, and execute it title_query.prepare(cmd) title_query.exec_() title_model = QSqlQueryModel() # Adds our queried information into a read-only model title_model.setQuery(title_query) # Adds information from out query model to a QTableView to be seen self.title_view = QTableView(self) self.title_view.setModel(title_model) self.title_view.resizeColumnToContents(0) # Modifying sizing of our QTableView new_title_width = self.title_view.columnWidth(0) + 45 self.title_view_label.move(int((self.width() - new_title_width) / 2), 580) self.title_view.resize(new_title_width, 130) self.title_view.move(int((self.width() - new_title_width) / 2), 605) self.title_view.doubleClicked.connect(lambda: self.pass_info( self.title_view.selectionModel().currentIndex())) # Allow users to go to new related recipe self.show() def pass_info(self, index): """ PURPOSE ----- To pass related recipe information in next window INPUT ----- index: The index of the row+col selected by the user when they double click the recipe view OUTPUT ----- calls function within RecipeWindow object we create in __init()__ """ # Get current indexed position from click cell_val = index.sibling(index.row(), index.column()).data() # Get cell value from the cell itself self.recipe_title = cell_val self.title_view.hide() self.hide() self.view_recipe(self.obtain_recipe())
class FreezeTableWidget(QTableView): def __init__(self, model): super(FreezeTableWidget, self).__init__() self.setModel(model) self.frozenTableView = QTableView(self) self.init() self.horizontalHeader().sectionResized.connect(self.updateSectionWidth) self.verticalHeader().sectionResized.connect(self.updateSectionHeight) self.frozenTableView.verticalScrollBar().valueChanged.connect( self.verticalScrollBar().setValue) self.verticalScrollBar().valueChanged.connect( self.frozenTableView.verticalScrollBar().setValue) def init(self): self.frozenTableView.setModel(self.model()) self.frozenTableView.setFocusPolicy(Qt.NoFocus) self.frozenTableView.verticalHeader().hide() self.frozenTableView.horizontalHeader().setSectionResizeMode( QHeaderView.Fixed) self.viewport().stackUnder(self.frozenTableView) self.frozenTableView.setStyleSheet(''' QTableView { border: none; background-color: #8EDE21; selection-background-color: #999; }''') # for demo purposes self.frozenTableView.setSelectionModel(self.selectionModel()) for col in range(1, self.model().columnCount()): self.frozenTableView.setColumnHidden(col, True) self.frozenTableView.setColumnWidth(0, self.columnWidth(0)) self.frozenTableView.setHorizontalScrollBarPolicy( Qt.ScrollBarAlwaysOff) self.frozenTableView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.frozenTableView.show() self.updateFrozenTableGeometry() self.setHorizontalScrollMode(self.ScrollPerPixel) self.setVerticalScrollMode(self.ScrollPerPixel) self.frozenTableView.setVerticalScrollMode(self.ScrollPerPixel) def updateSectionWidth(self, logicalIndex, oldSize, newSize): if self.logicalIndex == 0: self.frozenTableView.setColumnWidth(0, newSize) self.updateFrozenTableGeometry() def updateSectionHeight(self, logicalIndex, oldSize, newSize): self.frozenTableView.setRowHeight(logicalIndex, newSize) def resizeEvent(self, event): super(FreezeTableWidget, self).resizeEvent(event) self.updateFrozenTableGeometry() def moveCursor(self, cursorAction, modifiers): current = super(FreezeTableWidget, self).moveCursor(cursorAction, modifiers) if (cursorAction == self.MoveLeft and self.current.column() > 0 and self.visualRect(current).topLeft().x() < self.frozenTableView.columnWidth(0)): newValue = (self.horizontalScrollBar().value() + self.visualRect(current).topLeft().x() - self.frozenTableView.columnWidth(0)) self.horizontalScrollBar().setValue(newValue) return current def scrollTo(self, index, hint): if index.column() > 0: super(FreezeTableWidget, self).scrollTo(index, hint) def updateFrozenTableGeometry(self): self.frozenTableView.setGeometry( self.verticalHeader().width() + self.frameWidth(), self.frameWidth(), self.columnWidth(0), self.viewport().height() + self.horizontalHeader().height())
class RecipeTitleWindow(QWidget): def __init__(self): super().__init__() self.search_text = "" self.search_category = "" self.title_view = QTableView(self) self.recipe_window = RecipeWindow() # Create connection with SQlite3 language self.conn = QSqlDatabase.addDatabase("QSQLITE") self.conn.setDatabaseName("recipes.db") def recipe_search(self): """ PURPOSE ----- To search and display the recipes from the desired query from the user using sqlite3 on a table-view OUTPUT ----- Displays a table of rows containing recipe titles that the user can double click on to view information about it """ self.conn.open() # Open our connection to database self.setWindowTitle("Recipe Application") if len(self.search_text.strip(' ')) == 0: # If the query is empty, throw an error message error_dialog = QErrorMessage() error_dialog.showMessage( f"It seems you haven't input anything to search for the {self.search_category.lower()}'s associated recipes! Please try again.") error_dialog.exec_() else: # Otherwise we proceed as normally self.search_list = self.search_text.split(',') # Create an array of desired ingredients recipe_query = "" # Empty string to construct our query for elements in self.search_list: # Here we construct our query through a for-loop # There shouldn't be many elements in our array in the first place, so no worries of efficiency recipe_query += f"`{self.search_category}` LIKE '%" + elements + "%' AND " if len(recipe_query) > 5: # On the off chance that there is nothing input into the text box, throw error recipe_query = recipe_query[:len(recipe_query) - 5] # to remove last AND cmd = \ f""" SELECT `Recipe Title` FROM recipes R WHERE {recipe_query} """ title_query = QSqlQuery(self.conn) # Establish our query, prepare, and execute it title_query.prepare(cmd) title_query.exec_() title_model = QSqlQueryModel() # Adds our queried information into a read-only model title_model.setQuery(title_query) if title_model.rowCount() == 0: # If no recipes return, throw error message saying there exists no recipe # with their desired information empty_error_dialog = QErrorMessage() empty_error_dialog.showMessage( f"It appears that there are no recipes pertaining to your search. Please try again!") empty_error_dialog.exec_() else: # Adds information from out query model to a QTableView to be seen self.title_view.setModel(title_model) self.title_view.setWindowTitle("Recipe List") self.title_view.doubleClicked.connect(lambda: self.pass_info(self.title_view.selectionModel().currentIndex())) # Adds functionality of view_recipe when double clicking cell self.title_view.setMaximumHeight(500) # Set max height of table + window self.setMaximumHeight(500) self.title_view.resizeColumnToContents(0) # Sets size of column to contents, i.e. longest recipe title # Each cell is ~ 30 pixels, so we make our window slightly smaller than the rows can fit if it # is less than 16 cells (which populates ~ 500 pixel height) to keep width consistent w/ scroll bar # and to make sure we have no empty space in our window self.title_view.resize(self.title_view.columnWidth(0) + 40, 25*title_model.rowCount()) self.resize(self.title_view.columnWidth(0) + 40, 25*title_model.rowCount()) self.title_view.show() self.show() def pass_info(self, index): # Get current indexed position from click """ PURPOSE ----- To pass recipe information in next window, 'RecipeWindow' INPUT ----- index: The index of the row+col selected by the user when they double click the recipe view OUTPUT ----- calls function within RecipeWindow object we create in __init()__ """ cell_val = index.sibling(index.row(), index.column()).data() # Get cell value from the cell itself self.recipe_window.recipe_title = cell_val # Sets information in RecipeWindow object self.title_view.hide() self.hide() # Hides our current window self.recipe_window.view_recipe(self.recipe_window.obtain_recipe())
class FreezeTableMixin: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._frozen_table = QTableView(self) self._frozen_table.verticalHeader().hide() self._frozen_table.setFocusPolicy(Qt.NoFocus) self._frozen_table.setStyleSheet(''' QTableView { border: none; background-color: palette(dark); alternate-background-color: palette(mid); color: palette(base); } ''') self._frozen_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self._frozen_table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self._frozen_table.horizontalHeader().setStretchLastSection(True) self._frozen_table.horizontalHeader().setSortIndicator( -1, Qt.AscendingOrder) self.viewport().stackUnder(self._frozen_table) self._frozen_table.verticalHeader().setDefaultSectionSize( self.verticalHeader().defaultSectionSize()) self._frozen_table.hide() self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self._frozen_table.setVerticalScrollMode( QAbstractItemView.ScrollPerPixel) # connect the headers and scrollbars of both tableviews together self.horizontalHeader().sectionResized.connect( self.updateFrozenSectionWidth) self._frozen_table.horizontalHeader().sectionResized.connect( self.updateTableSectionWidth) self.verticalHeader().sectionResized.connect(self.updateSectionHeight) self._frozen_table.verticalScrollBar().valueChanged.connect( self.verticalScrollBar().setValue) self.verticalScrollBar().valueChanged.connect( self._frozen_table.verticalScrollBar().setValue) def frozenTable(self): return self._frozen_table def setHorizontalHeader(self, header: QHeaderView) -> None: header.sectionResized.connect(self.updateFrozenSectionWidth) header.setSortIndicator(-1, Qt.AscendingOrder) super().setHorizontalHeader(header) header.stackUnder(self._frozen_table) def setFrozenTableHorizontalHeader(self, header: QHeaderView) -> None: header.sectionResized.connect(self.updateTableSectionWidth) header.setSortIndicator(-1, Qt.AscendingOrder) self._frozen_table.setHorizontalHeader(header) def setVerticalHeader(self, header: QHeaderView) -> None: header.sectionResized.connect(self.updateSectionHeight) self._frozen_table.verticalHeader().setDefaultSectionSize( header.defaultSectionSize()) super().setVerticalHeader(header) def setModel(self, model: QAbstractItemModel) -> None: super().setModel(model) # Derive a proxy model from the model to limit number of columns frozen_model = RearrangeColumnsProxymodel(self) frozen_model.setSourceModel(self.model()) self._frozen_table.setModel(frozen_model) link_selection_model = LinkItemSelectionModel( frozen_model, QAbstractItemView.selectionModel(self), self) self._frozen_table.setSelectionModel(link_selection_model) # noinspection PyUnusedLocal def updateFrozenSectionWidth(self, logical_index: int, old_size: int, new_size: int): model = self._frozen_table.model() if model is None: return proxy_logical_index = model.proxyColumnForSourceColumn(logical_index) if proxy_logical_index > 0: self._frozen_table.horizontalHeader().blockSignals(True) self._frozen_table.horizontalHeader().resizeSection( proxy_logical_index, new_size) self._frozen_table.horizontalHeader().blockSignals(False) self.updateFrozenTableGeometry() # noinspection PyUnusedLocal def updateTableSectionWidth(self, logical_index: int, old_size: int, new_size: int): model = self._frozen_table.model() if model is None: return source_logical_index = model.sourceColumnForProxyColumn(logical_index) if source_logical_index > 0: self.setColumnWidth(source_logical_index, new_size) self.updateFrozenTableGeometry() # noinspection PyUnusedLocal def updateSectionHeight(self, logical_index: int, old_size: int, new_size: int): self._frozen_table.setRowHeight(logical_index, new_size) def resizeEvent(self, event): QTableView.resizeEvent(self, event) self.updateFrozenTableGeometry() def scrollTo( self, index: QModelIndex, hint: QAbstractItemView.ScrollHint = QAbstractItemView.EnsureVisible ) -> None: if index.column() > 1: QTableView.scrollTo(self, index, hint) def setFrozenColumns(self, num_columns=None): if num_columns is not None: model = self._frozen_table.model() if model is None: return mapping = [ self.horizontalHeader().logicalIndex(col) for col in range(num_columns) ] mapping = [col for col in mapping if not self.isColumnHidden(col)] model.setSourceColumns(mapping) # Synchronize section sizes between table and frozen table hh = self._frozen_table.horizontalHeader() for col in range(num_columns): logical_index = model.sourceColumnForProxyColumn(col) hh.resizeSection(col, self.columnWidth(logical_index)) self._frozen_table.show() self.updateFrozenTableGeometry() else: self._frozen_table.hide() def updateFrozenTableGeometry(self): model = self._frozen_table.model() if model is None: return ax = ay = self.frameWidth() aw = sum( self.columnWidth(model.sourceColumnForProxyColumn(i)) for i in range(model.columnCount())) ah = self.viewport().height() + self.horizontalHeader().height() if self.verticalHeader().isVisible(): ax += self.verticalHeader().width() self._frozen_table.setGeometry(ax, ay, aw, ah) def moveCursor(self, cursorAction, modifiers): current = QTableView.moveCursor(self, cursorAction, modifiers) x = self.visualRect(current).topLeft().x() frozen_width = self._frozen_table.columnWidth( 0) + self._frozen_table.columnWidth(1) if cursorAction == self.MoveLeft and current.column( ) > 1 and x < frozen_width: new_value = self.horizontalScrollBar().value() + x - frozen_width self.horizontalScrollBar().setValue(new_value) return current
class TaggerDialog(QDialog): def __init__(self, args, **kwargs): super(TaggerDialog, self).__init__(**kwargs) self.reviewing = False self.args = args self.worker = TaggerWorker() self.thread = QThread() self.worker.moveToThread(self.thread) self.worker.on_error.connect(self.on_error) self.worker.on_review_ready.connect(self.on_review_ready) self.worker.on_stopped.connect(self.on_stopped) self.worker.on_progress.connect(self.on_progress) self.worker.on_updates_sent.connect(self.on_updates_sent) self.worker.on_mint_mfa.connect(self.on_mint_mfa) self.thread.started.connect( partial(self.worker.create_updates, args, self)) self.thread.start() self.init_ui() def init_ui(self): self.setWindowTitle('Tagger is running...') self.setModal(True) self.v_layout = QVBoxLayout() self.setLayout(self.v_layout) self.label = QLabel() self.v_layout.addWidget(self.label) self.progress = 0 self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 0) self.v_layout.addWidget(self.progress_bar) self.button_bar = QHBoxLayout() self.v_layout.addLayout(self.button_bar) self.cancel_button = QPushButton('Cancel') self.button_bar.addWidget(self.cancel_button) self.cancel_button.clicked.connect(self.on_cancel) def on_error(self, msg): logger.error(msg) self.label.setText('Error: {}'.format(msg)) self.label.setStyleSheet('QLabel { color: red; font-weight: bold; }') self.cancel_button.setText('Close') self.cancel_button.clicked.connect(self.close) def open_amazon_order_id(self, order_id): if order_id: QDesktopServices.openUrl(QUrl(amazon.get_invoice_url(order_id))) def on_activated(self, index): # Only handle clicks on the order_id cell. if index.column() != 5: return order_id = self.updates_table_model.data(index, Qt.DisplayRole) self.open_amazon_order_id(order_id) def on_double_click(self, index): if index.column() == 5: # Ignore double clicks on the order_id cell. return order_id_cell = self.updates_table_model.createIndex(index.row(), 5) order_id = self.updates_table_model.data(order_id_cell, Qt.DisplayRole) self.open_amazon_order_id(order_id) def on_review_ready(self, results): self.reviewing = True self.progress_bar.hide() self.label.setText('Select below which updates to send to Mint.') self.updates_table_model = MintUpdatesTableModel(results.updates) self.updates_table = QTableView() self.updates_table.doubleClicked.connect(self.on_double_click) self.updates_table.clicked.connect(self.on_activated) def resize(): self.updates_table.resizeColumnsToContents() self.updates_table.resizeRowsToContents() min_width = sum( self.updates_table.columnWidth(i) for i in range(6)) self.updates_table.setMinimumSize(min_width + 20, 600) self.updates_table.setSelectionMode(QAbstractItemView.SingleSelection) self.updates_table.setSelectionBehavior(QAbstractItemView.SelectRows) self.updates_table.setModel(self.updates_table_model) self.updates_table.setSortingEnabled(True) resize() self.updates_table_model.layoutChanged.connect(resize) self.v_layout.insertWidget(2, self.updates_table) unmatched_button = QPushButton('View Unmatched Amazon orders') self.button_bar.addWidget(unmatched_button) unmatched_button.clicked.connect( partial(self.on_open_unmatched, results.unmatched_orders)) amazon_stats_button = QPushButton('Amazon Stats') self.button_bar.addWidget(amazon_stats_button) amazon_stats_button.clicked.connect( partial(self.on_open_amazon_stats, results.items, results.orders, results.refunds)) tagger_stats_button = QPushButton('Tagger Stats') self.button_bar.addWidget(tagger_stats_button) tagger_stats_button.clicked.connect( partial(self.on_open_tagger_stats, results.stats)) self.confirm_button = QPushButton('Send to Mint') self.button_bar.addWidget(self.confirm_button) self.confirm_button.clicked.connect(self.on_send) self.setGeometry(50, 50, self.width(), self.height()) def on_updates_sent(self, num_sent): self.label.setText( 'All done! {} newly tagged Mint transactions'.format(num_sent)) self.cancel_button.setText('Close') def on_open_unmatched(self, unmatched): self.unmatched_dialog = AmazonUnmatchedTableDialog(unmatched) self.unmatched_dialog.show() def on_open_amazon_stats(self, items, orders, refunds): self.amazon_stats_dialog = AmazonStatsDialog(items, orders, refunds) self.amazon_stats_dialog.show() def on_open_tagger_stats(self, stats): self.tagger_stats_dialog = TaggerStatsDialog(stats) self.tagger_stats_dialog.show() def on_send(self): self.progress_bar.show() updates = self.updates_table_model.get_selected_updates() self.confirm_button.hide() self.updates_table.hide() self.confirm_button.deleteLater() self.updates_table.deleteLater() self.adjustSize() QMetaObject.invokeMethod(self.worker, 'send_updates', Qt.QueuedConnection, Q_ARG(list, updates), Q_ARG(object, self.args)) def on_stopped(self): self.close() def on_progress(self, msg, max, value): self.label.setText(msg) self.progress_bar.setRange(0, max) self.progress_bar.setValue(value) def on_cancel(self): if not self.reviewing: QMetaObject.invokeMethod(self.worker, 'stop', Qt.QueuedConnection) else: self.close() def on_mint_mfa(self): mfa_code, ok = QInputDialog().getText(self, 'Please enter your Mint Code.', 'Mint Code:') QMetaObject.invokeMethod(self.worker, 'mfa_code', Qt.QueuedConnection, Q_ARG(int, mfa_code))
class DBViewer(QDialog): resized = pyqtSignal(object) def __init__(self, ctx, par, *args, **kwargs): super(DBViewer, self).__init__(*args, **kwargs) self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowFlags(self.windowFlags() | Qt.WindowSystemMenuHint | Qt.WindowMinMaxButtonsHint) self.ctx = ctx self.par = par self.layout = QVBoxLayout() self.query_model = QSqlQueryModel() self.toolbar = QToolBar() self.table_view = QTableView() self.table_view.setSelectionBehavior(QAbstractItemView.SelectRows) self.export_excel = QPushButton( self.ctx.export_icon, 'Export to Excel', ) self.refresh_btn = QPushButton("Refresh") self.close_btn = QPushButton("Close") self.delete_rows_btn = QPushButton("Delete Selected Row(s)") self.delete_rows_btn.setEnabled(False) self.initModel() self.initUI() self.sigConnect() def initUI(self): self.layout.setContentsMargins(11, 0, 11, 11) self.toolbar.addWidget(self.export_excel) self.layout.addWidget(self.toolbar) self.close_btn.setAutoDefault(True) self.close_btn.setDefault(True) self.refresh_btn.setAutoDefault(False) self.refresh_btn.setDefault(False) self.delete_rows_btn.setAutoDefault(False) self.delete_rows_btn.setDefault(False) self.export_excel.setAutoDefault(False) self.export_excel.setDefault(False) self.table_view.resizeColumnsToContents() self.layout.addWidget(self.table_view) hLayout = QHBoxLayout() hLayout.addWidget(self.delete_rows_btn) hLayout.addStretch() hLayout.addWidget(self.refresh_btn) hLayout.addWidget(self.close_btn) self.layout.addLayout(hLayout) self.setLayout(self.layout) self.setWindowTitle("Patients Record") wds = [ self.table_view.columnWidth(c) for c in range(self.table_view.model().columnCount()) ] self.resize(sum(wds) + 40, 600) rect = self.frameGeometry() rect.moveCenter(QDesktopWidget().availableGeometry().center()) self.move(rect.topLeft().x(), rect.topLeft().y()) def sigConnect(self): self.close_btn.clicked.connect(self.accept) self.refresh_btn.clicked.connect(self.on_refresh) self.delete_rows_btn.clicked.connect(self.on_delete_rows) self.export_excel.clicked.connect(self.on_export) self.resized.connect(self.on_window_resize) self.table_view.horizontalHeader().sectionResized.connect( self.on_column_resize) def initModel(self): self.table_model = QSqlTableModel(db=self.ctx.database.patient_db) self.table_model.setTable('patients') self.table_model.setEditStrategy(QSqlTableModel.OnFieldChange) self.table_model.select() self.table_view.setModel(self.table_model) self.table_view.selectionModel().selectionChanged.connect( self.on_rows_selected) def on_export(self): filename, _ = QFileDialog.getSaveFileName(self, "Export to Excel", "", "Excel Workbook (*.xlsx)") if not filename: return workbook = Workbook(filename) worksheet = workbook.add_worksheet() bold = workbook.add_format({'bold': True}) sql = "SELECT * FROM PATIENTS" self.query_model.setQuery(sql, self.ctx.database.patient_db) for row in range(self.query_model.rowCount() + 1): for col in range(self.query_model.record(row).count()): if row == 0: worksheet.write(row, col, self.query_model.record().fieldName(col), bold) worksheet.write(row + 1, col, self.query_model.record(row).value(col)) workbook.close() QMessageBox.information(self, "Success", "Records can be found in " + filename + " .") def on_refresh(self): self.initModel() def on_column_resize(self, id, oldsize, size): width = self.size().width() self.column_ratio[id] = size / width def on_window_resize(self, event): old_width = event.oldSize().width() width = event.size().width() if old_width == -1: self.column_ratio = [ self.table_view.columnWidth(c) / width for c in range(self.table_view.model().columnCount()) ] else: self.table_view.horizontalHeader().sectionResized.disconnect( self.on_column_resize) [ self.table_view.setColumnWidth(c, r * width) for c, r in enumerate(self.column_ratio) ] self.table_view.horizontalHeader().sectionResized.connect( self.on_column_resize) def on_rows_selected(self): self.selected_rows = sorted( set(index.row() for index in self.table_view.selectedIndexes())) print(self.selected_rows) self.delete_rows_btn.setEnabled(len(self.selected_rows) != 0) def on_delete_rows(self): result = [] for row in self.selected_rows: res = self.table_model.removeRow(row) result.append(res) if not all(result): print(self.table_model.lastError()) self.ctx.records_count -= len(self.selected_rows) self.delete_rows_btn.setEnabled(False) self.on_refresh() self.par.info_panel.no_edit.setText(str(self.ctx.records_count + 1)) def resizeEvent(self, event): self.resized.emit(event) return super(DBViewer, self).resizeEvent(event)
class TableDemo(QWidget): def __init__(self, parent=None): super(TableDemo, self).__init__(parent) self.setWindowTitle('TableView Demo') self.resize(600, 500) modelVerticalTitleLabelList = ['A', 'B', 'C', 'D', 'E'] modelHorizontalTitleLabelList = ['甲', '乙', '丙', '丁', '戊', '已'] self.model = QStandardItemModel(5, 5) self.model.setHorizontalHeaderLabels(modelVerticalTitleLabelList) self.model.setVerticalHeaderLabels(modelHorizontalTitleLabelList) for i in range(5): for j in range(5): item = QStandardItem(str(i + j)) self.model.setItem(i, j, item) self.tabletView = QTableView() self.tabletView.setModel(self.model) self.tabletView.horizontalHeader().setStretchLastSection(True) mainLayout = QVBoxLayout() mainLayout.addWidget(self.tabletView) self.testRowColumn() self.setLayout(mainLayout) def testRowColumn(self): # 用rowAt()和columnAt(),返回一个坐标在table中的行和列的序号 x, y = 100, 200 row_at_x = self.tabletView.rowAt(x) column_at_y = self.tabletView.columnAt(y) print('坐标(%d,%d)在表格中是第%d行,第%d列' % (x, y, row_at_x + 1, column_at_y + 1)) # 如果(x,y)不在表格内,函数返回值是-1 # setRowHeight,setColumnWidth:可以设置行高'列宽 self.tabletView.setRowHeight(1, 50) self.tabletView.setColumnWidth(1, 150) # <int> columnWidth():返回指定列的宽度 # <int> rowHeight():返回指定行的高度 print('第(2,2)单元格的列宽,行高为为:%d,%d(像素)' % (self.tabletView.columnWidth(1), self.tabletView.rowHeight(1))) # setSpan():设定指定行和列的行跨度和列跨度 self.tabletView.setSpan(2, 2, 2, 2) # <int> rowSpan():返回指定行的位置的行跨度 # <int> columnSpan:返回指定(row,column)的列跨度 print( '第(3,3)单元格的的行和列跨度为(%dx%d)' % (self.tabletView.columnSpan(2, 2), self.tabletView.rowSpan(2, 2))) # setCornerButtonEnable():设置是否启用左上角的按钮 # 此按钮(用来全选整个表格),默认是启用的 self.tabletView.setCornerButtonEnabled(False) # 此时,左上角的按钮将不再起作用 def contextMenuEvent(self, QContextMenuEvent): # 设置tableView的右键弹出菜单和菜单信号槽 # 如何只在单元格内弹出右键菜单呢?现在在空白地方点击右键也会弹出菜单???????? menu = QMenu(self) hideMenu = menu.addAction('&Hide') hideMenu.triggered.connect(self.hideCurrentColumn) # 设置显示所有被隐藏的列 showhiddenColumnMenu = menu.addAction('显示隐藏列') showhiddenColumnMenu.triggered.connect(self.showAllHiddenColumns) menu.addSeparator() # 设置当前点击的列按照内容自适应列宽度 resizeColumnToCtnsMenu = menu.addAction('宽度适应') resizeColumnToCtnsMenu.triggered.connect( lambda: self.tabletView.resizeColumnToContents( self.tabletView.currentIndex().column())) # 排序当前选择的列 orderCurentColumnMenu = menu.addAction('排序') orderCurentColumnMenu.triggered.connect(self.orderCurrentColum) menu.exec_(QContextMenuEvent.globalPos()) def hideCurrentColumn(self): print('第%d列被隐藏了!' % self.tabletView.currentIndex().column()) self.tabletView.setColumnHidden( self.tabletView.currentIndex().column(), True) def showAllHiddenColumns(self): print('显示所有被隐藏的列') # 遍历所有的列,找到隐藏的列,设置其隐藏为False for i in range(self.model.columnCount()): if self.tabletView.isColumnHidden(i): self.tabletView.setColumnHidden(i, False) print('列%d已被重新显示' % (i + 1)) def orderCurrentColum(self): self.tabletView.setSortingEnabled(True) self.tabletView.sortByColumn(self.tabletView.currentIndex().column(), Qt.AscendingOrder)
class QuoteMonitor(QMainWindow): def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.rowHeight = 30 # self.colInsWidth = 200 # self.colWidth = 100 sshFile = "style.qss" # sshFile = "material.qss" with open(sshFile, "r") as f: self.setStyleSheet(f.read()) self._defaultColWidths = {"instrument": 200} self._defaultColWidthOther = 100 self.initUI() if g.state: print("set column width from state") columnsWidth = g.state["table"]["columnsWidth"] geometry = g.state["geometry"] left = geometry["left"] top = geometry["top"] width = geometry["width"] height = geometry["height"] self.setGeometry(left, top, width, height) for i, v in enumerate(columnsWidth): self.table.setColumnWidth(i, v) def initUI(self): self.table = QTableView() self.delegate = Delegate() if g.state: cols = g.state["table"]["columns"] for i, v in enumerate(cols): if v == "instrument": g.colEditable = i self.table.setItemDelegateForColumn(i, self.delegate) break # self.table.verticalHeader().setSectionsMovable(True) self.table.horizontalHeader().setSectionsMovable(True) # self.table.horizontalHeader().sectionResized.connect( # lambda a, b, c: self.resizeColumns(self.size(), self.size())) # self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.table.horizontalHeader().setStretchLastSection(True) self.table.setDragEnabled(True) self.table.setSelectionBehavior(self.table.SelectRows) self.table.setSelectionMode(self.table.SingleSelection) self.table.setDragDropMode(self.table.InternalMove) self.table.setDragDropOverwriteMode(False) self.table.setDropIndicatorShown(True) self.table.setAcceptDrops(True) self.table.setShowGrid(False) self.table.setAlternatingRowColors(True) self.table.setStyle(TableStyle()) self.table.setMouseTracking(True) self.table.setContextMenuPolicy(Qt.CustomContextMenu) self.table.customContextMenuRequested.connect(self.showContextMenu) self.table.setModel(g.quoteModel) if not g.state: print("set column width from not state") accumWidth = 0 for i in range(g.quoteModel.columnCount()): colName = g.quoteModel.columnName(i) colWidth = self._defaultColWidths.get( colName, self._defaultColWidthOther) accumWidth += colWidth self.table.setColumnWidth(i, colWidth) self.resize(accumWidth + 20, 500) self.setCentralWidget(self.table) self.menubar = QMenuBar() self.fileMenu = self.menubar.addMenu("File") action = QAction("Preferences", self) self.fileMenu.addAction(action) self.fileMenu.triggered.connect(self.processtrigger) # menubar.setGeometry(300, 300, 300, 200) # mainLayout = QHBoxLayout() # mainLayout.addWidget(view) # self.setLayout(mainLayout) # self.setWindowTitle("Quote Monitor") # def showEvent(self, ev): # self.appendedRowsIfNeeded() # # self.setWidgetWidth() # self.initColumnSizes() def showContextMenu(self, pos): self.contextMenu = QMenu(self) action = QAction("remove", self) action.triggered.connect(self.removeRow) self.contextMenu.addAction(action) self.contextMenu.exec(self.mapToGlobal(pos)) def removeRow(self): item = self.table.currentIndex() row = item.row() g.quoteModel.removeRow(row) def processtrigger(self): self.dialog = Dialog(self) self.dialog.show() def appendedRowsIfNeeded(self): # L.debug("appended row if neened") self.height = QMainWindow.height(self) visibleRows = self.height / self.rowHeight rowsToAdd = ceil(visibleRows - g.quoteModel.rowCount()) rowsToAdd = max(rowsToAdd, 0) for _ in range(rowsToAdd): g.quoteModel.appendRow() def initColumnSizes(self): numCols = g.quoteModel.columnCount() colWidth = round(self.width() / numCols) accumWidth = 0 for i in range(numCols - 1): accumWidth += colWidth self.table.setColumnWidth(i, colWidth) self.table.setColumnWidth(numCols - 1, self.width() - accumWidth) # def resizeColumns(self, size, oldSize): # numCols = g.quoteModel.columnCount() # ratio = size.width() / oldSize.width() # accumWidth = 0 # for i in range(numCols - 1): # widthNow = round(self.table.columnWidth(i) * ratio) # accumWidth += widthNow # self.table.setColumnWidth(i, widthNow) # self.table.setColumnWidth(numCols - 1, self.width() - accumWidth) def resizeEvent(self, ev): self.appendedRowsIfNeeded() # self.resizeColumns(ev.size(), ev.oldSize()) def keyPressEvent(self, ev): if ev.key() == Qt.Key_Backspace: self.removeRow() def closeEvent(self, ev): self.saveApplicationState() writeToJSONFile(g.stateFile, g.state) # def sortIndicatorChanged(self, column=None, sortOrder=None): # print("sortIndicatorChanged: column {}, sortOrder {}".format(column, # sortOrder)) def saveApplicationState(self): geom = self.geometry() columnsWidth = [] header = [] # for i in range(g.quoteModel.columnCount()): # print(g.quoteModel.headerData(i, Qt.Horizontal, Qt.DisplayRole)) for i in range(g.quoteModel.columnCount()): idx = self.table.horizontalHeader().logicalIndex(i) header.insert(i, g.quoteModel.columnName(idx)) width = self.table.columnWidth(idx) columnsWidth.insert(i, width) state = {} state["table"] = table = {} state["geometry"] = geometry = {} table["rows"] = g.quoteModel.getRows() table["columns"] = header table["data"] = { str(k): { "instrument": v["instrument"] } for k, v in g.quoteModel._data.items() if k } table["columnsWidth"] = columnsWidth geometry["left"] = geom.left() geometry["top"] = geom.top() geometry["width"] = geom.width() geometry["height"] = geom.height() g.state = state
class FileManager(QWidget): def __init__(self, parent): QWidget.__init__(self) self.parent = parent self.name = 'File Manager' self.port = '9080' self.server = None drives = win32api.GetLogicalDriveStrings().split('\\\000')[:-1] self.logical_drives = drives + [d+'/' for d in drives] # create file manager tab self.file_manager_layout = QGridLayout(self) # create left manager (PC) self.left_up_btn = QPushButton() self.left_up_btn.setIcon(QIcon('images/up_btn.png')) self.left_up_btn.setFixedWidth(25) self.file_manager_layout.addWidget(self.left_up_btn, 0, 0, 1, 1) self.left_dir_path = QLineEdit(self.parent.expanduser_dir) self.file_manager_layout.addWidget(self.left_dir_path, 0, 1, 1, 8) self.left_go_to_btn = QPushButton() self.left_go_to_btn.setIcon(QIcon('images/right_btn.png')) self.left_go_to_btn.setFixedWidth(25) self.file_manager_layout.addWidget(self.left_go_to_btn, 0, 9, 1, 1) self.lefttableview = QTableView() self.lefttableview.setSelectionBehavior(QTableView.SelectRows) self.lefttableview.verticalHeader().hide() self.lefttableview.setShowGrid(False) self.lefttableview.contextMenuEvent = lambda event: self.left_context(event) self.left_file_model = QFileSystemModel() self.left_file_model.setFilter(QDir.AllEntries | QDir.NoDotAndDotDot) self.left_file_model.setRootPath(self.parent.expanduser_dir) self.left_file_model_path = self.parent.expanduser_dir self.lefttableview.setModel(self.left_file_model) self.lefttableview.setColumnWidth(0, 150) self.lefttableview.setRootIndex(self.left_file_model.index(self.parent.expanduser_dir)) self.file_manager_layout.addWidget(self.lefttableview, 1, 0, 5, 10) # central buttons self.download_file_from_device_btn = QPushButton() self.download_file_from_device_btn.setIcon(QIcon('images/left_btn.png')) self.download_file_from_device_btn.setFixedWidth(30) self.download_file_from_device_btn.setEnabled(False) self.upload_file_to_device_btn = QPushButton() self.upload_file_to_device_btn.setIcon(QIcon('images/right_btn.png')) self.upload_file_to_device_btn.setFixedWidth(30) self.upload_file_to_device_btn.setEnabled(False) self.delete_file_btn = QPushButton() self.delete_file_btn.setIcon(QIcon('images/delete_btn.png')) self.delete_file_btn.setFixedWidth(30) self.file_manager_layout.addWidget(self.download_file_from_device_btn, 3, 10, 1, 1) self.file_manager_layout.addWidget(self.delete_file_btn, 4, 10, 1, 1) # create right manager (Device) self.right_up_btn = QPushButton() self.right_up_btn.setIcon(QIcon('images/up_btn.png')) self.right_up_btn.setFixedWidth(25) self.right_up_btn.setEnabled(False) self.file_manager_layout.addWidget(self.right_up_btn, 0, 11, 1, 1) self.add_folder_btn = QPushButton() self.add_folder_btn.setIcon(QIcon('images/folder_add.png')) self.add_folder_btn.setFixedWidth(25) self.add_folder_btn.setToolTip(_('Add new folder')) self.add_folder_btn.setEnabled(False) self.file_manager_layout.addWidget(self.add_folder_btn, 0, 12, 1, 1) self.right_dir_path = QLineEdit() self.file_manager_layout.addWidget(self.right_dir_path, 0, 13, 1, 7) self.right_update_btn = QPushButton() self.right_update_btn.setIcon(QIcon('images/update.png')) self.right_update_btn.setFixedWidth(25) self.file_manager_layout.addWidget(self.right_update_btn, 0, 20, 1, 1) self.righttableview = QTableView() self.righttableview.setSelectionBehavior(QTableView.SelectRows) self.righttableview.contextMenuEvent = lambda event: self.right_context(event) self.righttableview.verticalHeader().hide() self.righttableview.setShowGrid(False) self.right_file_model = QStandardItemModel() self.right_file_model_path = [] self.right_active_dir = None self.righttableview.setModel(self.right_file_model) self.file_manager_layout.addWidget(self.righttableview, 1, 11, 5, 10) # auto sync self.timer = QTimer() self.timer.setInterval(10000) self.file_models_auto_sync = QCheckBox(_('Auto sync')) self.left_file_model_auto_sync_label = QLineEdit() self.left_file_model_auto_sync_label.setReadOnly(True) self.right_file_model_auto_sync_label = QLineEdit() self.right_file_model_auto_sync_label.setReadOnly(True) self.file_manager_layout.addWidget(self.file_models_auto_sync, 6, 9, 1, 3, alignment=Qt.AlignCenter) self.file_manager_layout.addWidget(self.left_file_model_auto_sync_label, 6, 0, 1, 9) self.file_manager_layout.addWidget(self.right_file_model_auto_sync_label, 6, 12, 1, 9) self.timer.timeout.connect(lambda: self.check_device_sync()) self.lefttableview.clicked.connect(lambda idx: self.left_file_model_clicked(idx)) self.lefttableview.doubleClicked.connect(lambda idx: self.left_file_model_doubleclicked(idx)) self.left_up_btn.clicked.connect(lambda: self.left_file_model_up(self.left_file_model.index(self.left_dir_path.text()))) self.left_go_to_btn.clicked.connect(lambda: self.left_file_model_go_to_dir()) self.right_update_btn.clicked.connect(lambda: self.right_file_model_update()) self.righttableview.doubleClicked.connect(lambda idx: self.right_file_model_doubleclicked(idx)) self.right_up_btn.clicked.connect(lambda: self.right_file_model_up()) self.add_folder_btn.clicked.connect(lambda: self.right_file_model_add_folder()) self.righttableview.clicked.connect(lambda idx: self.right_file_model_clicked(idx)) self.download_file_from_device_btn.clicked.connect(lambda: self.download_file_from_device()) self.upload_file_to_device_btn.clicked.connect(lambda: self.upload_file_to_device()) self.delete_file_btn.clicked.connect(lambda: self.delete_file_from_file_model()) self.parent.settings_widget.signal_ip_changed.connect(lambda ip: self.change_ip(ip)) self.parent.signal_language_changed.connect(lambda: self.retranslate()) def retranslate(self): self.file_models_auto_sync.setText(_('Auto sync')) self.right_file_model.setHorizontalHeaderLabels([_('Name'), _('Size'), _('Changed date')]) def change_ip(self, ip): self.server = ':'.join([ip, self.port]) self.right_file_model_path = [] self.right_file_model.clear() def left_file_model_clicked(self, idx): if os.path.isfile(self.left_file_model.filePath(idx)) and self.parent.geoshark_widget.device_on_connect: self.upload_file_to_device_btn.setEnabled(True) else: self.upload_file_to_device_btn.setEnabled(False) def left_file_model_doubleclicked(self, idx): self.left_up_btn.setEnabled(True) fileinfo = self.left_file_model.fileInfo(idx) if fileinfo.isDir(): self.lefttableview.setRootIndex(idx) self.left_dir_path.setText(self.left_file_model.filePath(idx)) self.left_file_model_path = self.left_file_model.filePath(idx) def left_file_model_up(self, idx): self.upload_file_to_device_btn.setEnabled(False) if self.left_dir_path.text() in self.logical_drives: self.left_file_model = QFileSystemModel() self.left_file_model.setFilter(QDir.AllEntries | QDir.NoDotAndDotDot) self.left_file_model.setRootPath('') self.lefttableview.setModel(self.left_file_model) self.left_dir_path.setText('My computer') self.left_up_btn.setEnabled(False) else: fileinfo = self.left_file_model.fileInfo(idx) dir = fileinfo.dir() self.left_dir_path.setText(dir.path()) self.left_file_model_path = dir.path() self.lefttableview.setRootIndex(self.left_file_model.index(dir.absolutePath())) def left_file_model_go_to_dir(self): if os.path.isdir(self.left_dir_path.text()): self.left_file_model_path = self.left_dir_path.text() self.left_up_btn.setEnabled(True) self.upload_file_to_device_btn.setEnabled(False) self.left_file_model.setRootPath(self.left_dir_path.text()) self.lefttableview.setRootIndex(self.left_file_model.index(self.left_dir_path.text())) def right_file_model_update(self): if not self.parent.geoshark_widget.device_on_connect: return url = 'http://{}/active_dir'.format(self.server) try: res = requests.get(url, timeout=5) if res.ok: self.right_active_dir = res.text except requests.exceptions.RequestException: pass file_list = self.get_folder_list() if file_list is None: return self.fill_right_file_model(file_list) self.download_file_from_device_btn.setEnabled(False) def get_folder_list(self, folder_path=None): if self.server is None: return if folder_path is None: folder_path = '/'.join(self.right_file_model_path) url = 'http://{}/data/{}'.format(self.server, folder_path) try: res = requests.get(url, timeout=1) except requests.exceptions.RequestException: show_error(_('GeoShark error'), _('GeoShark is not responding.')) return if res.ok: res = res.json() return res else: return None def check_device_sync(self): pc_path = self.left_file_model_auto_sync_label.text() device_path = self.right_file_model_auto_sync_label.text() if self.file_models_auto_sync.isChecked() and pc_path != '' and device_path != '': file_list = self.get_folder_list(self.right_file_model_auto_sync_label.text()) left_list_of_files = os.listdir(self.left_file_model_auto_sync_label.text()) for f in file_list: if f['name'] not in left_list_of_files or os.path.getsize('{}/{}'.format(pc_path, f['name'])) != f['size']: self.download_file_from_device(device_path='{}/{}'.format(device_path, f['name']), pc_path=pc_path) def fill_right_file_model(self, directory): self.add_folder_btn.setEnabled(True) if len(self.right_file_model_path) < 1: self.right_up_btn.setEnabled(False) else: self.right_up_btn.setEnabled(True) self.add_folder_btn.setEnabled(False) self.right_file_model.removeRows(0, self.right_file_model.rowCount()) self.right_dir_path.setText('/'.join(self.right_file_model_path)) self.right_file_model.setHorizontalHeaderLabels([_('Name'), _('Size'), _('Changed date')]) for row, instance in enumerate(directory): if instance['name'] == self.right_active_dir: image = QIcon('images/directory_active.png') else: image = QIcon('images/{}.png'.format(instance['type'])) item = QStandardItem(image, instance['name']) item.setData(instance['type'], 5) item.setEditable(False) self.right_file_model.setItem(row, 0, item) item = QStandardItem(str(instance['size'])) item.setEditable(False) self.right_file_model.setItem(row, 1, item) item = QStandardItem(str(datetime.datetime.fromtimestamp(instance['changed']).strftime('%d.%m.%Y %H:%M'))) item.setEditable(False) self.right_file_model.setItem(row, 2, item) self.righttableview.setColumnWidth(0, max(150, self.righttableview.columnWidth(0))) def left_context(self, event): context_menu = {} index = self.lefttableview.indexAt(event.pos()) if index.row() == -1: return context_menu[_('Set active directory')] = lambda: self.set_pc_active_directory(self.left_file_model.filePath(index)) context_menu[_('Remove element')] = lambda: self.delete_file_from_file_model(index) if not self.left_file_model.isDir(index): del context_menu[_('Set active directory')] menu = QMenu() actions = [QAction(a) for a in context_menu.keys()] menu.addActions(actions) action = menu.exec_(event.globalPos()) if action: context_menu[action.text()]() def set_pc_active_directory(self, path): self.left_file_model_auto_sync_label.setText(path) self.parent.settings_widget.left_folder_tracked.setText(path) def right_context(self, event): context_menu = {} index = self.righttableview.indexAt(event.pos()) if index.row() == -1: return item = self.right_file_model.itemFromIndex(index) item_row = item.row() context_menu[_('Set active directory')] = lambda: self.set_active_directory(item) context_menu[_('Remove element')] = lambda: self.delete_file_from_file_model(index) if self.right_file_model.item(item_row, 0).data(5) != 'directory': del context_menu[_('Set active directory')] menu = QMenu() actions = [QAction(a) for a in context_menu.keys()] menu.addActions(actions) action = menu.exec_(event.globalPos()) if action: context_menu[action.text()]() def set_active_directory(self, item): if not self.parent.geoshark_widget.device_on_connect: return dirname = item.text() url = 'http://{}/active_dir'.format(self.server) try: res = requests.post(url=url, data=dirname, timeout=5) except requests.exceptions.RequestException: show_error(_('GeoShark error'), _('Can not set active directory.\nGeoShark is not responding.')) return if res.ok: self.right_file_model_update() self.set_active_path(dirname) elif res.status_code == 400: show_error(_('GeoShark error'), _('Request declined - request body specifies invalid path.')) return elif res.status_code == 409: show_error(_('GeoShark error'), _('Request declined - switching active directory is forbidden during active session.')) return else: print(res.status_code) return def set_active_path(self, dirname): path = '/'.join(self.right_file_model_path + [dirname]) self.parent.settings_widget.right_folder_tracked.setText(path) self.right_file_model_auto_sync_label.setText(path) def right_file_model_clicked(self, idx): if not self.parent.geoshark_widget.device_on_connect: return if self.right_file_model.item(idx.row(), 0).data(5) == 'file': self.download_file_from_device_btn.setEnabled(True) else: self.download_file_from_device_btn.setEnabled(False) def right_file_model_doubleclicked(self, idx): if not self.parent.geoshark_widget.device_on_connect: return model_path = '/'.join(self.right_file_model_path) idx_name = self.right_file_model.item(idx.row(), 0).text() if model_path != '': dir = '{}/{}'.format(model_path, idx_name) else: dir = '{}'.format(idx_name) file_list = self.get_folder_list(dir) if file_list is None: return self.right_file_model_path = dir.split('/') self.fill_right_file_model(file_list) def right_file_model_up(self): if not self.parent.geoshark_widget.device_on_connect: return self.download_file_from_device_btn.setEnabled(False) up_dir = '/'.join(self.right_file_model_path[:-1]) file_list = self.get_folder_list(up_dir) if file_list is None: return if up_dir == '': self.right_file_model_path = [] else: self.right_file_model_path = up_dir.split('/') self.fill_right_file_model(file_list) def right_file_model_add_folder(self): if not self.parent.geoshark_widget.device_on_connect: return row = self.right_file_model.rowCount() item = QStandardItem(QIcon('images/folder.png'), 'New Directory') item.setData('directory', 5) item.setEditable(True) self.right_file_model.setItem(row, 0, item) item = QStandardItem(str(0.0)) item.setEditable(False) self.right_file_model.setItem(row, 1, item) item = QStandardItem(str(datetime.datetime.today().strftime('%d.%m.%Y %H:%M'))) item.setEditable(False) self.right_file_model.setItem(row, 2, item) def download_file_from_device(self, device_path=None, pc_path=None): if not self.parent.geoshark_widget.device_on_connect or self.server is None: return if not device_path: fileName = self.find_selected_idx() if fileName: fileName = fileName.data() device_path = '/'.join(self.right_file_model_path + [fileName]) else: return right_file_model_filename = device_path.split('/')[-1] save_to_file = '{}/{}'.format(self.left_file_model_path, right_file_model_filename) \ if not pc_path else '{}/{}'.format(pc_path, right_file_model_filename) if os.path.isfile(save_to_file): answer = show_warning_yes_no(_('File warning'), _('There is a file with the same name in PC.\n' 'Do you want to rewrite <b>{}</b>?'.format(right_file_model_filename))) if answer == QMessageBox.No: return url = 'http://{}/data/{}'.format(self.server, device_path) try: b = bytearray() res = requests.get(url, timeout=5, stream=True) if res.ok: progress = QProgressBar() progress.setFormat(right_file_model_filename) self.file_manager_layout.addWidget(progress, 6, 12, 1, 9) total_length = int(res.headers.get('content-length')) len_b = 0 for chunk in tee_to_bytearray(res, b): len_b += len(chunk) progress.setValue((len_b/total_length)*99) QApplication.processEvents() else: return except: self.file_manager_layout.addWidget(self.right_file_model_auto_sync_label, 6, 12, 1, 9) show_error(_('GeoShark error'), _('GeoShark is not responding.')) return if res.ok: progress.setValue(100) with open(save_to_file, 'wb') as file: file.write(b) for i in reversed(range(self.file_manager_layout.count())): if isinstance(self.file_manager_layout.itemAt(i).widget(), QProgressBar): self.file_manager_layout.itemAt(i).widget().setParent(None) self.file_manager_layout.addWidget(self.right_file_model_auto_sync_label, 6, 12, 1, 9) def upload_file_to_device(self): if not self.parent.geoshark_widget.device_on_connect or self.server is None: return file = self.left_file_model.filePath(self.lefttableview.currentIndex()) filename = file.split('/')[-1] url = 'http://{}/data/{}'.format(self.server, '/'.join(self.right_file_model_path)) filesize = os.path.getsize(file) if filesize == 0: show_error(_('File error'), _('File size must be non zero.')) return progress = ProgressBar(text=_('Upload File Into GeoShark'), window_title=_('Upload file to GeoShark')) encoder = MultipartEncoder( fields={'upload_file': (filename, open(file, 'rb'))} # added mime-type here ) data = MultipartEncoderMonitor(encoder, lambda monitor: progress.update((monitor.bytes_read/filesize)*99)) try: res = requests.post(url, data=data, headers={'Content-Type': encoder.content_type}, timeout=5) except requests.exceptions.RequestException: progress.close() show_error(_('GeoShark error'), _('GeoShark is not responding.')) return if res.ok: progress.update(100) self.right_file_model_update() def delete_file_from_file_model(self, index=None): selected = self.find_selected_idx() if index is None and selected is None: return if index is None: index = selected model = index.model() index_row = index.row() path = model.filePath(index) if hasattr(model, 'filePath') else model.index(index_row, 0).data() answer = show_warning_yes_no(_('Remove File warning'), _('Do you really want to remove:\n{}').format(path)) if answer == QMessageBox.No: return if isinstance(model, QFileSystemModel): model.remove(index) elif isinstance(model, QStandardItemModel): if not self.parent.geoshark_widget.device_on_connect or self.server is None: return filename = self.right_file_model.item(index.row(), 0).text() path = '/'.join(self.right_file_model_path + [filename]) url = 'http://{}/data/{}'.format(self.server, path) try: res = requests.delete(url) except requests.exceptions.RequestException: show_error(_('GeoShark error'), _('GeoShark is not responding.')) return if res.ok: self.right_file_model_update() elif res.status_code == 400: self.right_file_model.removeRow(index.row()) elif res.status_code == 409: show_error(_('GeoShark error'), _('Request declined - directory is the part of active session working directory.')) return def find_selected_idx(self): left_indexes = self.lefttableview.selectedIndexes() right_indexes = self.righttableview.selectedIndexes() if len(left_indexes) == 0 and len(right_indexes) == 0: return None index = left_indexes[0] if len(left_indexes) > len(right_indexes) else right_indexes[0] return index def save_file_models_folder(self): self.left_file_model_auto_sync_label.setText(self.parent.settings_widget.left_folder_tracked.text()) self.right_file_model_auto_sync_label.setText(self.parent.settings_widget.right_folder_tracked.text())
class ERUploaderWindow(QDialog): """This window provides the user a visual picture of the local and online status of the Extra Reflectance calibration file repository. It also allows uploading of files that are present locally but not on the server. It does not have good handling of edge cases, e.g. online server in inconsistent state.""" def __init__(self, manager: ERManager, parent: Optional[QWidget] = None): self._dataComparator = ERDataComparator(manager.onlineDirectory, manager.localDirectory) self._manager = manager self._selectedId: str = None super().__init__(parent) self.setModal(False) self.setWindowTitle("Extra Reflectance File Manager") self.setLayout(QVBoxLayout()) self.table = QTableView(self) self.fileStatus = self._dataComparator.compare() self.table.setModel(PandasModel(self.fileStatus)) self.table.setSelectionMode(QTableView.SingleSelection) self.table.setSelectionBehavior(QTableView.SelectRows) self.table.customContextMenuRequested.connect(self.openContextMenu) self.table.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.uploadButton = QPushButton("Upload to Drive") self.uploadButton.released.connect(self._updateGDrive) self.uploadButton.setToolTip( "Upload any files for which the status is `Local Only` to the google drive account." ) self.refreshButton = QPushButton('Refresh') self.refreshButton.setToolTip( "Rescan the files in the applications data directory.") self.refreshButton.released.connect(self.refresh) self.updateIndexButton = QPushButton("Update Index File") self.updateIndexButton.setToolTip( "Download the online index file and merge it with our local index file." ) self.updateIndexButton.released.connect(self._updateIndexFile) self.layout().addWidget(self.table) l = QHBoxLayout() l.setContentsMargins(0, 0, 0, 0) l.addWidget(self.uploadButton) l.addWidget(self.updateIndexButton) l.addWidget(self.refreshButton) w = QWidget() w.setLayout(l) self.layout().addWidget(w) self.table.setColumnWidth(0, 300) self.table.setColumnWidth(1, 150) self.table.setColumnWidth(2, 150) self.table.setMinimumWidth( sum( self.table.columnWidth(i) for i in range(self.table.model().columnCount())) + self.table.verticalHeader().width() + 20) def displayInfo(self, index: QModelIndex): msg = QMessageBox.information(self, 'Info', repr(self.fileStatus.iloc[index.row()])) def plotData(self, index: QModelIndex): idTag = self.fileStatus.iloc[index.row()]['idTag'] md = self._dataComparator.local.getMetadataFromId(idTag) erCube = ExtraReflectanceCube.fromMetadata(md) self.plotHandle = PlotNd(erCube.data) def openContextMenu(self, pos: QPoint): """This method opens a context menu, it should be called when the user right clicks.""" index = self.table.indexAt(pos) row = self.fileStatus.iloc[index.row()] menu = QMenu() displayAction = QAction("Display Info") displayAction.triggered.connect(lambda: self.displayInfo(index)) menu.addAction(displayAction) if row['Local Status'] == self._dataComparator.local.DataStatus.found.value: plotAction = QAction("Plot Local Data") plotAction.triggered.connect(lambda: self.plotData(index)) menu.addAction(plotAction) menu.exec(self.mapToGlobal(pos)) def _updateGDrive(self): """Checks for all files taht are present locally but not on the server. Uploads those file and then overwrites the index.""" try: status = self.fileStatus if not np.all((status['Index Comparison'] == ERDataComparator.ComparisonStatus.LocalOnly.value) | (status['Index Comparison'] == ERDataComparator.ComparisonStatus.Match.value)): raise ValueError( "Uploading cannot be performed if the local index file is not valid. Try updating the index file." ) uploadableRows = ( status['Index Comparison'] == ERDataComparator.ComparisonStatus.LocalOnly.value) | ( status['Online Status'] == ERDataDirectory.DataStatus.missing.value) if np.any(uploadableRows): # There is something to upload for i, row, in status.loc[uploadableRows].iterrows(): fileName = [ i.fileName for i in self._dataComparator.local.index.cubes if i.idTag == row['idTag'] ][0] self._manager.upload(fileName) self._manager.upload('index.json') self.refresh() except Exception as e: logger = logging.getLogger(__name__) logger.exception(e) mess = QMessageBox.information(self, 'Sorry', str(e)) def refresh(self): """Scans local and online files to refresh the display.""" self._dataComparator.updateIndexes() self.fileStatus = self._dataComparator.compare() self.table.setModel(PandasModel(self.fileStatus)) def _updateIndexFile(self): self._dataComparator.online.updateIndex() index = ERIndex.merge(self._dataComparator.local.index, self._dataComparator.online.index) self._dataComparator.local.saveNewIndex(index) self.refresh()
class FreezeTableWidget(QTableView): def __init__(self, model): super(FreezeTableWidget, self).__init__() self.setModel(model) self.frozenTableView = QTableView(self) self.init() self.horizontalHeader().sectionResized.connect(self.updateSectionWidth) self.verticalHeader().sectionResized.connect(self.updateSectionHeight) self.frozenTableView.verticalScrollBar().valueChanged.connect( self.verticalScrollBar().setValue) self.verticalScrollBar().valueChanged.connect( self.frozenTableView.verticalScrollBar().setValue) def init(self): self.frozenTableView.setModel(self.model()) self.frozenTableView.setFocusPolicy(Qt.NoFocus) self.frozenTableView.verticalHeader().hide() self.frozenTableView.horizontalHeader().setSectionResizeMode( QHeaderView.Fixed) self.viewport().stackUnder(self.frozenTableView) self.frozenTableView.setStyleSheet(''' QTableView { border: none; background-color: #8EDE21; selection-background-color: #999; }''') # for demo purposes self.frozenTableView.setSelectionModel(self.selectionModel()) for col in range(1, self.model().columnCount()): self.frozenTableView.setColumnHidden(col, True) self.frozenTableView.setColumnWidth(0, self.columnWidth(0)) self.frozenTableView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.frozenTableView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.frozenTableView.show() self.updateFrozenTableGeometry() self.setHorizontalScrollMode(self.ScrollPerPixel) self.setVerticalScrollMode(self.ScrollPerPixel) self.frozenTableView.setVerticalScrollMode(self.ScrollPerPixel) def updateSectionWidth(self, logicalIndex, oldSize, newSize): if self.logicalIndex == 0: self.frozenTableView.setColumnWidth(0, newSize) self.updateFrozenTableGeometry() def updateSectionHeight(self, logicalIndex, oldSize, newSize): self.frozenTableView.setRowHeight(logicalIndex, newSize) def resizeEvent(self, event): super(FreezeTableWidget, self).resizeEvent(event) self.updateFrozenTableGeometry() def moveCursor(self, cursorAction, modifiers): current = super(FreezeTableWidget, self).moveCursor(cursorAction, modifiers) if (cursorAction == self.MoveLeft and self.current.column() > 0 and self.visualRect(current).topLeft().x() < self.frozenTableView.columnWidth(0)): newValue = (self.horizontalScrollBar().value() + self.visualRect(current).topLeft().x() - self.frozenTableView.columnWidth(0)) self.horizontalScrollBar().setValue(newValue) return current def scrollTo(self, index, hint): if index.column() > 0: super(FreezeTableWidget, self).scrollTo(index, hint) def updateFrozenTableGeometry(self): self.frozenTableView.setGeometry( self.verticalHeader().width() + self.frameWidth(), self.frameWidth(), self.columnWidth(0), self.viewport().height() + self.horizontalHeader().height())
class Widget(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) self.setGeometry(100, 150, 1200, 600) self.Columns = '*' self.font = None self.HeaderList = None self.left_padding = 0 self.right_padding = 0 self.search_field_spacing = 5 self.createDB() self.getHeaders() layout = QVBoxLayout(self) buttons_layout = QHBoxLayout() show_plant_view = QPushButton('Plant photo with details') font_button = QPushButton('Font') buttons_layout.addWidget(show_plant_view) buttons_layout.addWidget(font_button) layout.addLayout(buttons_layout) show_plant_view.clicked.connect(self._show_photo_view) font_button.clicked.connect(self.font_dialog) self.model = MySqlModel(self.HeaderList) self.SearchQuery = "select {} from {}".format(self.Columns, _DB_TABLE) self.model.setQuery(self.SearchQuery, self.DB) self.view = QTableView(self) self.view.setMinimumHeight(325) self.font = self.view.property('font') self.view.setModel(self.model) self.view.setColumnHidden(self.model.image_column, True) self.view.setColumnHidden(self.model.info_column, True) horizontal_header = self.view.horizontalHeader() for i in range(len(horizontal_header)): horizontal_header.setSectionResizeMode(i, QHeaderView.Stretch) self.search_layout = QHBoxLayout() self.search_layout.insertSpacing(0, self.left_padding) self.SearchFieldsGrid = QGridLayout() self.makeSearchFieldsGrid() self.search_layout.addLayout(self.SearchFieldsGrid) self.search_layout.insertSpacing(-1, self.right_padding) layout.addLayout(self.search_layout) #self.view.horizontalHeader().setStretchLastSection(True) #self.view.resizeColumnsToContents() #self.view.horizontalHeader().ResizeMode(QHeaderView.Interactive) layout.addWidget(self.view) self.view.resizeRowsToContents() self.view_vertical_header = self.view.verticalHeader() self.view_vertical_header.geometriesChanged.connect( self._align_search_layout) def createDB(self): # binding to an existing database self.DB = QtSql.QSqlDatabase.addDatabase('QSQLITE') self.DB.setDatabaseName(_PATH_TO_DB) self.DB.open() def getHeaders(self): # getting a list of Headers self.query = QtSql.QSqlQuery(db=self.DB) self.query.exec_("PRAGMA table_info({})".format(_DB_TABLE)) # filling the list of headings self.HeaderList = [] while self.query.next(): self.HeaderList.append(self.query.value(1)) # create a query parameter dictionary self.paramsDict = {x: ['', '', ''] for x in self.HeaderList} self.paramsDict[''] = ["{} {} '%{}%'", '', ''] def makeSearchFieldsGrid(self): self.SearchFieldsGrid.setSpacing(self.search_field_spacing) self.clearLayout(self.SearchFieldsGrid) self.cb = QCheckBox(self) self.cb.stateChanged.connect(self.changeQuery) self.SearchFieldsGrid.addWidget(self.cb, 1, 0) n = len(self.HeaderList) qwidth = [self.view.columnWidth(i) for i in range(n)] self.qles = [None for i in range(n)] for i in range(n): self.qles[i] = QLineEdit(self) self.qles[i].setObjectName(self.HeaderList[i]) self.qles[i].textChanged[str].connect(self.setSearchQuery) label = QLabel(self.HeaderList[i]) self.SearchFieldsGrid.addWidget(label, 0, i + 1, alignment=Qt.AlignCenter) self.SearchFieldsGrid.addWidget(self.qles[i], 1, i + 1) def clearLayout(self, layout): while layout.count(): child = layout.takeAt(0) if child.widget() is not None: child.widget().deleteLater() elif child.layout() is not None: clearLayout(child.layout()) # remembering the status of the switch after pressing and updating the table def changeQuery(self, state): self.state = state self.setSearchQuery('') def setSearchQuery(self, text): # switch handling try: if self.state == Qt.Checked: self.paramsDict[''] = [ "{0} {1} '% {2}%' or {0} {1} '{2}%'", '', '' ] else: self.paramsDict[''] = ["{} {} '%{}%'", '', ''] except: self.paramsDict[''] = ["{} {} '%{}%'", '', ''] # processing of more and less characters if text != '': if text[0] == '<': matching = '<' queryString = "{} {} {}" text = text[1:] elif text[0] == '>': matching = '>' queryString = "{} {} {}" text = text[1:] else: queryString = self.paramsDict[''][0] matching = 'like' else: queryString, matching, text = self.paramsDict[''] # filling in the query parameters dictionary self.paramsDict[self.sender().objectName()] = [ queryString, matching, text ] paramList = [] # assembling query parameters into a list for name, value in self.paramsDict.items(): if len(value) == 3: queryString, matching, text = value if queryString.find('%') != -1: queryString = self.paramsDict[''][0] if text != '': paramList.append(queryString.format(name, matching, text)) # assembling query parameters into a string if len(paramList) == 0: params = '' elif len(paramList) == 1: params = 'where {}'.format(paramList[0]) else: params = 'where {}'.format(" and ".join(paramList)) # assembling the query and updating the table according to it self.Columns = '*' if self.Columns == '' else self.Columns self.searchQuery = "select {} from {} {}".format( self.Columns, _DB_TABLE, params) self.model.setQuery(self.searchQuery, self.DB) self.view.resizeRowsToContents() #header = self.view.horizontalHeader() #for i in range(len(header)): #header.setSectionResizeMode(i, QHeaderView.Stretch) def font_dialog(self): self.font, valid = QFontDialog.getFont(self.font) if valid: self.view.setFont(self.font) self.view.resizeRowsToContents() for i in self.qles: i.setFont(self.font) def _align_search_layout(self): vertical_header_width = self.view_vertical_header.width() checkbox_width = self.cb.width() self.left_padding = vertical_header_width - checkbox_width - self.search_field_spacing vertical_scrollbar_width = self.view.verticalScrollBar().width() self.right_padding = vertical_scrollbar_width left_spacer = self.search_layout.itemAt(0) left_spacer.changeSize(self.left_padding, 10) right_spacer = self.search_layout.itemAt(2) right_spacer.changeSize(self.right_padding, 10) self.search_layout.invalidate() def _show_photo_view(self): #image_data = _get_image_paths_names(self.model) viewer = PhotoViewe(self.model, self) viewer.show()