def __init__( self, urlPatchServer, prodCode, language, runDir, patchClient, wineProgram, hiResEnabled, iconFileIn, homeDir, winePrefix, wineDebug, osType, parent, data_folder, current_game, gameDocumentsDir, ): self.homeDir = homeDir self.osType = osType self.logger = logging.getLogger("OneLauncher") ui_file = QtCore.QFile(os.path.join(data_folder, "ui", "winPatch.ui")) ui_file.open(QtCore.QFile.ReadOnly) loader = QUiLoader() self.winLog = loader.load(ui_file, parentWidget=parent) ui_file.close() self.winLog.setWindowFlags(QtCore.Qt.Dialog | QtCore.Qt.FramelessWindowHint) if self.osType.usingWindows: self.winLog.setWindowTitle("Output") else: self.winLog.setWindowTitle("Patch - Wine output") self.winLog.btnSave.setText("Save Log") self.winLog.btnSave.setEnabled(False) self.winLog.progressBar.reset() self.winLog.btnStop.setText("Close") self.winLog.btnStart.setText("Patch") self.winLog.btnSave.clicked.connect(self.btnSaveClicked) self.winLog.btnStop.clicked.connect(self.btnStopClicked) self.winLog.btnStart.clicked.connect(self.btnStartClicked) self.aborted = False self.finished = True self.lastRun = False self.command = "" self.arguments = [] self.process_status_timer = QtCore.QTimer() self.process_status_timer.timeout.connect( self.activelyShowProcessStatus) patchClient = os.path.join(runDir, patchClient) # Fix for the at least one person who has a title case patchclient.dll if os.path.split(patchClient)[ 1] == "patchclient.dll" and not os.path.exists(patchClient): patchClient = os.path.join(runDir, "PatchClient.dll") # Make sure patchClient exists if not os.path.exists(patchClient): self.winLog.txtLog.append( '<font color="Khaki">Patch client %s not found</font>' % (patchClient)) self.logger.error("Patch client %s not found" % (patchClient)) return self.progressMonitor = ProgressMonitor(self.winLog) self.process = QtCore.QProcess() self.process.readyReadStandardOutput.connect(self.readOutput) self.process.readyReadStandardError.connect(self.readErrors) self.process.finished.connect(self.processFinished) processEnvironment = QtCore.QProcessEnvironment.systemEnvironment() if self.osType.usingWindows: self.arguments = [ patchClient, "Patch", urlPatchServer, "--language", language, "--productcode", prodCode, ] self.command = "rundll32.exe" # Get log file to read patching details from, since # rundll32 doesn't provide output on Windows log_folder_name = gameDocumentsDir game_logs_folder = os.path.join( os.path.split(os.environ.get("APPDATA"))[0], "Local", log_folder_name, ) self.patch_log_file = os.path.join(game_logs_folder, "PatchClient.log") if os.path.exists(self.patch_log_file): os.remove(self.patch_log_file) open(self.patch_log_file, "x") self.patch_log_file = open(self.patch_log_file, "r") else: if winePrefix != "": processEnvironment.insert("WINEPREFIX", winePrefix) if wineDebug != "": processEnvironment.insert("WINEDEBUG", wineDebug) self.arguments = [ "rundll32.exe", patchClient, "Patch", urlPatchServer, "--language", language, "--productcode", prodCode, ] self.command = wineProgram self.process.setProcessEnvironment(processEnvironment) self.process.setWorkingDirectory(runDir) if hiResEnabled: self.arguments.append("--highres") self.file_arguments = self.arguments.copy() self.file_arguments.append("--filesonly")
def __init__( self, appName, clientType, argTemplate, account, server, ticket, chatServer, language, runDir, wineProgram, wineDebug, winePrefix, hiResEnabled, builtInPrefixEnabled, osType, homeDir, iconFileIn, crashreceiver, DefaultUploadThrottleMbps, bugurl, authserverurl, supporturl, supportserviceurl, glsticketlifetime, worldName, accountText, parent, data_folder, startupScripts, gameConfigDir, ): # Fixes binary path for 64-bit client if clientType == "WIN64": appName = "x64" + os.sep + appName self.homeDir = homeDir self.osType = osType self.worldName = worldName self.accountText = accountText self.parent = parent self.logger = logging.getLogger("OneLauncher") self.startupScripts = startupScripts self.gameConfigDirPath = os.path.join(osType.documentsDir, gameConfigDir) ui_file = QtCore.QFile(os.path.join(data_folder, "ui", "winLog.ui")) ui_file.open(QtCore.QFile.ReadOnly) loader = QUiLoader() self.winLog = loader.load(ui_file, parentWidget=parent) ui_file.close() self.winLog.setWindowFlags(QtCore.Qt.Dialog | QtCore.Qt.FramelessWindowHint) if self.osType.usingWindows: self.winLog.setWindowTitle("Output") else: self.winLog.setWindowTitle("Launch Game - Wine output") # self.winLog.btnStart.setVisible(False) self.winLog.btnStart.setText("Back") self.winLog.btnStart.setEnabled(False) self.winLog.btnSave.setText("Save") self.winLog.btnSave.setEnabled(False) self.winLog.btnStop.setText("Exit") self.winLog.btnStart.clicked.connect(self.btnStartClicked) self.winLog.btnSave.clicked.connect(self.btnSaveClicked) self.winLog.btnStop.clicked.connect(self.btnStopClicked) self.aborted = False self.finished = False self.command = "" self.arguments = [] gameParams = ( argTemplate.replace("{SUBSCRIPTION}", account) .replace("{LOGIN}", server) .replace("{GLS}", ticket) .replace("{CHAT}", chatServer) .replace("{LANG}", language) .replace("{CRASHRECEIVER}", crashreceiver) .replace("{UPLOADTHROTTLE}", DefaultUploadThrottleMbps) .replace("{BUGURL}", bugurl) .replace("{AUTHSERVERURL}", authserverurl) .replace("{GLSTICKETLIFETIME}", glsticketlifetime) .replace("{SUPPORTURL}", supporturl) .replace("{SUPPORTSERVICEURL}", supportserviceurl) ) if not hiResEnabled: gameParams = gameParams + " --HighResOutOfDate" self.process = QtCore.QProcess() self.process.readyReadStandardOutput.connect(self.readOutput) self.process.readyReadStandardError.connect(self.readErrors) self.process.finished.connect(self.resetButtons) if self.osType.usingWindows: self.command = appName self.process.setWorkingDirectory(runDir) os.chdir(runDir) for arg in gameParams.split(" "): self.arguments.append(arg) else: processEnvironment = QtCore.QProcessEnvironment.systemEnvironment() if wineDebug != "": processEnvironment.insert("WINEDEBUG", wineDebug) if winePrefix != "": processEnvironment.insert("WINEPREFIX", winePrefix) self.command = wineProgram self.process.setWorkingDirectory(runDir) self.arguments.append(appName) for arg in gameParams.split(" "): self.arguments.append(arg) # Applies needed settings for the builtin wine prefix if builtInPrefixEnabled: # Enables ESYNC if open file limit is high enough if os.path.exists("/proc/sys/fs/file-max"): with open("/proc/sys/fs/file-max") as file: file_data = file.read() if int(file_data) >= 524288: processEnvironment.insert("WINEESYNC", "1") # Enables FSYNC. It overides ESYNC and will only be used if # the required kernel patches are installed. processEnvironment.insert("WINEFSYNC", "1") # Adds dll overrides for directx, so dxvk is used instead of wine3d processEnvironment.insert("WINEDLLOVERRIDES", "d3d11=n;dxgi=n;d3d10=n") self.process.setProcessEnvironment(processEnvironment) self.winLog.txtLog.append("Connecting to server: " + worldName) self.winLog.txtLog.append("Account: " + accountText) self.winLog.txtLog.append("Game Directory: " + runDir) self.winLog.txtLog.append("Game Client: " + appName) self.winLog.show() self.runStatupScripts()
class QtFrontend(ViewSBFrontend): """ Qt Frontend that consumes packets for display. """ UI_NAME = 'qt' UI_DESCRIPTION = 'unstable GUI in Qt' # So, Qt's tree widgets require that column 0 have the expand arrow, but you _can_ change # where column 0 is displayed. # We want the summary column to have the expand arrow, so we'll swap it # with the sequence column in __init__(). COLUMN_SEQUENCE = 6 COLUMN_TIMESTAMP = 1 COLUMN_DEVICE = 2 COLUMN_ENDPOINT = 3 COLUMN_DIRECTION = 4 COLUMN_LENGTH = 5 COLUMN_SUMMARY = 0 COLUMN_STATUS = 7 COLUMN_DATA = 8 @staticmethod def reason_to_be_disabled(): try: import PySide6 except ImportError: return "PySide6 (Qt library) not available." return None @staticmethod def _stringify_list(lst): """ Tiny helper than runs the str constructor on every item in a list, but specifically handles two cases: 1) the object in question is None, which we instead want to display as an empty string, 2) the resulting string contains a null character, which Qt doesn't like, so we'll represent it to the user as, literally, \0. """ return [str(x).replace('\0', r'\0') if x is not None else '' for x in lst] def _create_item_for_packet(self, viewsb_packet): """ Creates a QTreeWidgetItem for a given ViewSBPacket. Args: viewsb_packet -- The ViewSBPacket to create the QTreeWidgetItem from. Returns a QTreeWidgetItem. """ def get_packet_string_array(viewsb_packet): """ Tiny helper to return and stringify the common fields used for the columns of tree items. """ direction = viewsb_packet.direction.name if viewsb_packet.direction is not None else '' length = len(viewsb_packet.data) if viewsb_packet.data is not None else '' return self._stringify_list([ viewsb_packet.summarize(), viewsb_packet.timestamp, viewsb_packet.device_address, viewsb_packet.endpoint_number, direction, length, viewsb_packet.sequence, viewsb_packet.summarize_status(), viewsb_packet.summarize_data() ]) + [viewsb_packet] item = QTreeWidgetItem(get_packet_string_array(viewsb_packet)) # Give the item a reference to the original packet object. item.setData(0, QtCore.Qt.UserRole, viewsb_packet) return item def _recursively_walk_packet(self, viewsb_packet): """ Recursively walks packet subordinates, batching QTreeWidgetItem.addChildren as much as possible. Args: viewsb_packet -- The top-level packet (as far as the caller's context is concerned). """ packet_item = self._create_item_for_packet(viewsb_packet) packet_children_list = [] for sub_packet in viewsb_packet.subordinate_packets: # Create the item for this packet, and recursively fill its children. packet_children_list.append(self._recursively_walk_packet(sub_packet)) packet_item.addChildren(packet_children_list) return packet_item def __init__(self): """ Sets up the Qt UI. """ super().__init__() QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) signal.signal(signal.SIGINT, signal.SIG_DFL) # fix SIGINT handling - cleanly exit on ctrl+c self.app = QApplication.instance() or QApplication([]) try: import qt_material qt_material.apply_stylesheet(self.app, 'light_blue.xml') except ImportError: pass self.ui_file = QtCore.QFile(os.path.dirname(os.path.realpath(__file__)) + '/qt.ui') self.loader = QUiLoader() self.loader.registerCustomWidget(ViewSBQTreeWidget) self.loader.registerCustomWidget(ViewSBHexView) self.window = self.loader.load(self.ui_file) # type: QMainWindow # Swap columns 0 and 5 to put the expand arrow on the summary column. self.window.usb_tree_widget.header().swapSections(self.COLUMN_SUMMARY, self.COLUMN_SEQUENCE) self.window.usb_tree_widget.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) self.window.update_timer = QtCore.QTimer() self.window.update_timer.timeout.connect(self._update) self.window.usb_tree_widget.currentItemChanged.connect(self._tree_current_item_changed) self.window.usb_tree_widget = self.window.usb_tree_widget self.window.usb_tree_widget.sortByColumn(0, Qt.SortOrder.AscendingOrder) def ready(self): """ Called when the backend is ready to stream. """ self.window.showMaximized() def _update(self): """ Called by the QTimer `update_timer`; collects packets the queue and adds them to the tree view. We use this instead of calling `handle_communications` and defining `handle_incoming_packet`, because adding items one at a time as we receive them is slower than batching them. Note: Since this is called via a QTimer signal, this method runs in the UI thread. """ # Handle exceptions if self._exception_conn.poll(): self.handle_exception(*self._exception_conn.recv()) # TODO: overide handle_exception to show a Qt dialog message # If the process manager told us to stop (which might happen if e.g. the backend exits), # then stop and exit. if self.termination_event.is_set(): self.app.closeAllWindows() packet_list = [] try: # Get as many packets as we can as quick as we can. while True: packet = self.data_queue.get_nowait() packet_list.append(packet) # But the instant it's empty, don't wait for any more; just send them to be processed. except multiprocessing.queues.Empty: pass finally: self.add_packets(packet_list) def _tree_current_item_changed(self, current_item, _previous_item): """ Handler for the QTreeWidget.currentItemChanged() signal that populates the side panels with detail fields and a hex representation of the current packet. """ # Clear the details widget. self.window.usb_details_tree_widget.clear() current_packet = current_item.data(0, QtCore.Qt.UserRole) # A list of 2-tuples: first element is a table title, and the second is usually a string:string dict. detail_fields = current_packet.get_detail_fields() if detail_fields: self.update_detail_fields(detail_fields) self.window.usb_hex_view.populate(current_packet.get_raw_data()) def update_detail_fields(self, detail_fields): """ Populates the detail view with the relevant fields for the selected packet. """ # Each table will have a root item in the details view. root_items = [] for table in detail_fields: title = table[0] root = QTreeWidgetItem([title]) children = [] fields = table[1] # The usual case: a str:str dict. if isinstance(fields, dict): for key, value in fields.items(): children.append(QTreeWidgetItem(self._stringify_list([key, value]))) # Sometimes it'll just be a 1-column list. elif isinstance(fields, list): for item in fields: children.append(QTreeWidgetItem(self._stringify_list([item]))) # Sometimes it'll just be a string, or a `bytes` instance. else: children.append(QTreeWidgetItem(self._stringify_list([fields]))) root.addChildren(children) # Add an empty "item" between each table. root_items.extend([root, QTreeWidgetItem([])]) self.window.usb_details_tree_widget.addTopLevelItems(root_items) self.window.usb_details_tree_widget.expandAll() self.window.usb_details_tree_widget.resizeColumnToContents(0) self.window.usb_details_tree_widget.resizeColumnToContents(1) def add_packets(self, viewsb_packets): """ Adds a list of top-level ViewSB packets to the tree. We're in the UI thread; every bit of overhead counts, so let's batch as much as possible. """ top_level_items_list = [] for viewsb_packet in viewsb_packets: # Create the item for this packet, and recursively fill its children. top_level_items_list.append(self._recursively_walk_packet(viewsb_packet)) self.window.usb_tree_widget.addTopLevelItems(top_level_items_list) def run(self): """ Overrides ViewSBFrontend.run(). """ self.wait_for_backend_ready() # TODO: is there a better value than 100 ms? Should it be configurable by the Analyzer? self.window.update_timer.start(100) self.app.exec_() self.stop() def stop(self): self.app.closeAllWindows() self.termination_event.set()
def __init__(self): """""" super().__init__() self.main = QUiLoader().load('main_window.ui', self) self.add_menu_theme(self.main, self.main.menuStyles)
def __init__(self): """Initialize the class.""" super().__init__() # Preflight self.check_for_updates(silent=True) self.ui = QUiLoader().load(resources.path("tailor.resources", "tailor.ui")) self.ui.setWindowIcon( QtGui.QIcon(str(resources.path("tailor.resources", "tailor.png"))) ) # store reference to this code in data tab self.ui.data.code = self # set up dirty timer self._dirty_timer = QtCore.QTimer() self._dirty_timer.timeout.connect(self.mark_project_dirty) # clear all program state self.clear_all() # Enable close buttons... self.ui.tabWidget.setTabsClosable(True) # ...but remove them for the table view for pos in QtWidgets.QTabBar.LeftSide, QtWidgets.QTabBar.RightSide: widget = self.ui.tabWidget.tabBar().tabButton(0, pos) if widget: widget.close() # connect button signals self.ui.add_column_button.clicked.connect(self.add_column) self.ui.add_calculated_column_button.clicked.connect(self.add_calculated_column) # connect menu items self.ui.actionQuit.triggered.connect(self.ui.close) self.ui.actionAbout_Tailor.triggered.connect(self.show_about_dialog) self.ui.actionNew.triggered.connect(self.new_project) self.ui.actionOpen.triggered.connect(self.open_project_dialog) self.ui.actionSave.triggered.connect(self.save_project_or_dialog) self.ui.actionSave_As.triggered.connect(self.save_as_project_dialog) self.ui.actionCheck_for_updates.triggered.connect(self.check_for_updates) self.ui.actionImport_CSV.triggered.connect(self.import_csv) self.ui.actionExport_CSV.triggered.connect(self.export_csv) self.ui.actionExport_Graph_to_PDF.triggered.connect( lambda: self.export_graph(".pdf") ) self.ui.actionExport_Graph_to_PNG.triggered.connect( lambda: self.export_graph(".png") ) self.ui.actionClose.triggered.connect(self.new_project) self.ui.actionAdd_column.triggered.connect(self.add_column) self.ui.actionAdd_calculated_column.triggered.connect( self.add_calculated_column ) self.ui.actionAdd_row.triggered.connect(self.add_row) self.ui.actionRemove_column.triggered.connect(self.remove_column) self.ui.actionRemove_row.triggered.connect(self.remove_row) self.ui.actionClear_Cell_Contents.triggered.connect(self.clear_selected_cells) # set up the open recent menu self.ui._recent_files_separator = self.ui.menuOpen_Recent.insertSeparator( self.ui.actionClear_Menu ) self.update_recent_files() self.ui.actionClear_Menu.triggered.connect(self.clear_recent_files_menu) # user interface events self.ui.data_view.horizontalHeader().sectionMoved.connect(self.column_moved) self.ui.tabWidget.currentChanged.connect(self.tab_changed) self.ui.tabWidget.tabCloseRequested.connect(self.close_tab) self.ui.name_edit.textEdited.connect(self.rename_column) self.ui.formula_edit.textEdited.connect(self.update_column_expression) self.ui.create_plot_button.clicked.connect(self.ask_and_create_plot_tab) # install event filter to capture UI events (which are not signals) # necessary to caputer closeEvent inside QMainWindow widget self.ui.installEventFilter(self) # Set standard shortcuts for menu items self.ui.actionNew.setShortcut(QtGui.QKeySequence.New) self.ui.actionOpen.setShortcut(QtGui.QKeySequence.Open) self.ui.actionClose.setShortcut(QtGui.QKeySequence.Close) self.ui.actionSave.setShortcut(QtGui.QKeySequence.Save) self.ui.actionSave_As.setShortcut(QtGui.QKeySequence.SaveAs) # Set other shortcuts for menu items self.ui.actionImport_CSV.setShortcut(QtGui.QKeySequence("Ctrl+I")) self.ui.actionImport_CSV_Into_Current_Project.setShortcut( QtGui.QKeySequence("Shift+Ctrl+I") ) self.ui.actionExport_CSV.setShortcut(QtGui.QKeySequence("Ctrl+E")) self.ui.actionExport_Graph_to_PDF.setShortcut(QtGui.QKeySequence("Ctrl+G")) self.ui.actionExport_Graph_to_PNG.setShortcut( QtGui.QKeySequence("Shift+Ctrl+G") ) # Create shortcut for return/enter keys for key in QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter: QtGui.QShortcut( QtGui.QKeySequence(key), self.ui.data_view, self.edit_or_move_down ) # Shortcut for backspace and delete: clear cell contents for key in QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete: QtGui.QShortcut( QtGui.QKeySequence(key), self.ui.data_view, self.clear_selected_cells ) # Start at (0, 0) self.ui.data_view.setCurrentIndex(self.data_model.createIndex(0, 0))
class Application(QtCore.QObject): """Main user interface for the tailor app. The user interface centers on the table containing the data values. A single DataModel is instantiated to hold the data. """ _project_filename = None _recent_files_actions = None _selected_col_idx = None _plot_num = 1 _is_dirty = False def __init__(self): """Initialize the class.""" super().__init__() # Preflight self.check_for_updates(silent=True) self.ui = QUiLoader().load(resources.path("tailor.resources", "tailor.ui")) self.ui.setWindowIcon( QtGui.QIcon(str(resources.path("tailor.resources", "tailor.png"))) ) # store reference to this code in data tab self.ui.data.code = self # set up dirty timer self._dirty_timer = QtCore.QTimer() self._dirty_timer.timeout.connect(self.mark_project_dirty) # clear all program state self.clear_all() # Enable close buttons... self.ui.tabWidget.setTabsClosable(True) # ...but remove them for the table view for pos in QtWidgets.QTabBar.LeftSide, QtWidgets.QTabBar.RightSide: widget = self.ui.tabWidget.tabBar().tabButton(0, pos) if widget: widget.close() # connect button signals self.ui.add_column_button.clicked.connect(self.add_column) self.ui.add_calculated_column_button.clicked.connect(self.add_calculated_column) # connect menu items self.ui.actionQuit.triggered.connect(self.ui.close) self.ui.actionAbout_Tailor.triggered.connect(self.show_about_dialog) self.ui.actionNew.triggered.connect(self.new_project) self.ui.actionOpen.triggered.connect(self.open_project_dialog) self.ui.actionSave.triggered.connect(self.save_project_or_dialog) self.ui.actionSave_As.triggered.connect(self.save_as_project_dialog) self.ui.actionCheck_for_updates.triggered.connect(self.check_for_updates) self.ui.actionImport_CSV.triggered.connect(self.import_csv) self.ui.actionExport_CSV.triggered.connect(self.export_csv) self.ui.actionExport_Graph_to_PDF.triggered.connect( lambda: self.export_graph(".pdf") ) self.ui.actionExport_Graph_to_PNG.triggered.connect( lambda: self.export_graph(".png") ) self.ui.actionClose.triggered.connect(self.new_project) self.ui.actionAdd_column.triggered.connect(self.add_column) self.ui.actionAdd_calculated_column.triggered.connect( self.add_calculated_column ) self.ui.actionAdd_row.triggered.connect(self.add_row) self.ui.actionRemove_column.triggered.connect(self.remove_column) self.ui.actionRemove_row.triggered.connect(self.remove_row) self.ui.actionClear_Cell_Contents.triggered.connect(self.clear_selected_cells) # set up the open recent menu self.ui._recent_files_separator = self.ui.menuOpen_Recent.insertSeparator( self.ui.actionClear_Menu ) self.update_recent_files() self.ui.actionClear_Menu.triggered.connect(self.clear_recent_files_menu) # user interface events self.ui.data_view.horizontalHeader().sectionMoved.connect(self.column_moved) self.ui.tabWidget.currentChanged.connect(self.tab_changed) self.ui.tabWidget.tabCloseRequested.connect(self.close_tab) self.ui.name_edit.textEdited.connect(self.rename_column) self.ui.formula_edit.textEdited.connect(self.update_column_expression) self.ui.create_plot_button.clicked.connect(self.ask_and_create_plot_tab) # install event filter to capture UI events (which are not signals) # necessary to caputer closeEvent inside QMainWindow widget self.ui.installEventFilter(self) # Set standard shortcuts for menu items self.ui.actionNew.setShortcut(QtGui.QKeySequence.New) self.ui.actionOpen.setShortcut(QtGui.QKeySequence.Open) self.ui.actionClose.setShortcut(QtGui.QKeySequence.Close) self.ui.actionSave.setShortcut(QtGui.QKeySequence.Save) self.ui.actionSave_As.setShortcut(QtGui.QKeySequence.SaveAs) # Set other shortcuts for menu items self.ui.actionImport_CSV.setShortcut(QtGui.QKeySequence("Ctrl+I")) self.ui.actionImport_CSV_Into_Current_Project.setShortcut( QtGui.QKeySequence("Shift+Ctrl+I") ) self.ui.actionExport_CSV.setShortcut(QtGui.QKeySequence("Ctrl+E")) self.ui.actionExport_Graph_to_PDF.setShortcut(QtGui.QKeySequence("Ctrl+G")) self.ui.actionExport_Graph_to_PNG.setShortcut( QtGui.QKeySequence("Shift+Ctrl+G") ) # Create shortcut for return/enter keys for key in QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter: QtGui.QShortcut( QtGui.QKeySequence(key), self.ui.data_view, self.edit_or_move_down ) # Shortcut for backspace and delete: clear cell contents for key in QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete: QtGui.QShortcut( QtGui.QKeySequence(key), self.ui.data_view, self.clear_selected_cells ) # Start at (0, 0) self.ui.data_view.setCurrentIndex(self.data_model.createIndex(0, 0)) # tests # def test(): # print(self.get_column_ordering()) # QtGui.QShortcut(QtGui.QKeySequence("Ctrl+X"), self.ui.data_view, test) # filename = "~/Desktop/meting1.csv" # dialog = CSVFormatDialog(filename) # dialog.exec() # sys.exit() # if dialog.exec() == QtWidgets.QDialog.Accepted: # ( # delimiter, # decimal, # thousands, # header, # skiprows, # ) = dialog.get_format_parameters() # self._do_import_csv( # filename, delimiter, decimal, thousands, header, skiprows # ) # self._do_import_csv(filename, ";", ",", ".", 0, 0) # import numpy as np # import pandas as pd # x = [1, 2, 3, 4, 5, np.nan] # y = [1, 4, np.nan, 8, 10, np.nan] # self.data_model.beginResetModel() # self.data_model._data = pd.DataFrame.from_dict({"x": x, "y": y}) # self.data_model.endResetModel() # self.create_plot_tab("x", "y") # plot_tab = self.ui.tabWidget.currentWidget() # plot_tab.model_func.setText("a * x + b") # plot_tab.fit_button.clicked.emit() # np.random.seed(1) # x = np.linspace(0, 10, 11) # y = np.random.normal(loc=x, scale=0.1 * x, size=len(x)) # self.data_model.beginResetModel() # self.data_model._data = pd.DataFrame.from_dict( # {"U": x, "I": y, "dU": 0.1 * x + 0.01, "dI": 0.1 * y + 0.01} # ) # self.data_model.endResetModel() # self.create_plot_tab("U", "I", "dU", "dI") # plot_tab = self.tabWidget.currentWidget() # plot_tab.model_func.setText("U0 * U + b") # plot_tab.fit_button.clicked.emit() # plot_tab.model_func.setText("(U0 * U + b") # plot_tab.fit_button.clicked.emit() # self.tabWidget.setCurrentIndex(0) # self.data_view.selectColumn(0) # self.name_edit.setText("U_0") # self.name_edit.textEdited.emit("U_0") # self.data_view.selectColumn(1) # self.name_edit.setText("I_0") # self.name_edit.textEdited.emit("I_0") # self.tabWidget.setCurrentIndex(1) # plot_tab.fit_button.clicked.emit() # self.add_calculated_column() # self.name_edit.setText("inv_U") # self.formula_edit.setText("1 / U") # self.create_plot_tab("inv_U", "I") # self.add_calculated_column() # self.name_edit.setText("P") # self.formula_edit.setText("U * I") # self.name_edit.textEdited.emit("P") # self.formula_edit.textEdited.emit("U * I") # self.data_view.selectColumn(0) # self.remove_column() # self.add_column() # self.name_edit.setText("P") # self.data_view.selectColumn(0) # # self.create_plot_tab("U", "Usq") # plot_tab = self.tabWidget.currentWidget() # # plot_tab.fit_start_box.setValue(4.5) # # plot_tab.fit_end_box.setValue(7.5) # # plot_tab.use_fit_domain.setChecked(True) # plot_tab.model_func.setText("a * U + b") # # for row in plot_tab._params.values(): # # row.itemAt(plot_tab._idx_value_box).widget().setValue(20) # plot_tab.fit_button.clicked.emit() # plot_tab.draw_curve_option.setCurrentIndex(2) # # plot_tab.use_fit_domain.setChecked(False) # # plot_tab.export_graph("test.png") # # plot_tab.export_graph("test.pdf") # # self.tabWidget.setCurrentIndex(0) # # self.data_model._data["I"] /= 2 # # plot_tab.update_plot() # # plot_tab.model_func.setText("a * U ** 2 + c") # # plot_tab.model_func.textEdited.emit("") # self.save_project("test.tlr") # self.clear_all() # self.load_project(pathlib.Path.home() / "Desktop" / "analyse-radon220.tlr") # self.save_project("test.tlr") # self.clear_all() # self.load_project("test.tlr") # self._do_import_csv( # "~/Desktop/importtest.csv", ",", ".", ",", 0, 0, create_new=False # ) def _set_view_and_selection_model(self): """Set up data view and selection model. Connects the table widget to the data model, sets up various behaviours and resets visual column ordering. """ self.ui.data_view.setModel(self.data_model) self.ui.data_view.setDragDropMode(self.ui.data_view.NoDragDrop) header = self.ui.data_view.horizontalHeader() header.setSectionsMovable(True) header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) header.setMinimumSectionSize(header.defaultSectionSize()) # reset column ordering. There is, apparently, no easy way to do this :'( for log_idx in range(self.data_model.columnCount()): # move sections in the correct position FROM LEFT TO RIGHT # so, logical indexes should be numbered [0, 1, 2, ... ] # >>> header.moveSection(from, to) vis_idx = header.visualIndex(log_idx) header.moveSection(vis_idx, log_idx) self.selection = self.ui.data_view.selectionModel() self.selection.selectionChanged.connect(self.selection_changed) def mark_project_dirty(self, is_dirty=True): """Mark project as dirty""" self._is_dirty = is_dirty self.update_window_title() if not is_dirty: # FIXME: this can be implemented much better by actually detecting changes. self._dirty_timer.start(DIRTY_TIMEOUT) def column_moved(self, logidx, oldidx, newidx): """Move column in reaction to UI signal. Dragging a column to a new location triggers execution of this method. Since the UI only reorders the column visually and does not change the underlying data, we will store the ordering in the data model. Args: logidx (int): the logical column index (index in the dataframe) oldidx (int): the old visual index newidx (int): the new visual index """ self.data_model._column_order = self.get_column_ordering() self.data_model.recalculate_all_columns() def get_column_ordering(self): """Return the visual order of logical columns in the table view. Returns a list of column indexes. The first index is the first (visual) column in the table view. The index points to a colum in the underlying data. So, if the underlying data has columns col0, col1, col2, col3, but you visually rearrange them as col3, col1, col0, col2, then this method will return [3, 1, 0, 2]. """ header = self.ui.data_view.horizontalHeader() n_columns = self.data_model.columnCount() return [header.logicalIndex(i) for i in range(n_columns)] def eventFilter(self, watched, event): """Catch PySide6 events. Events are signals without slots. That is, signals which cannot be connected to predefined endpoints. They can, however, be captured by an event filter. Args: watched (QtCore.QObject): the object which generated the event. event (QtCore.QEvent): the event object. Returns: boolean: True if the event is ignored, False otherwise. """ if watched is self.ui and event.type() == QtCore.QEvent.Close: if self.confirm_project_close_dialog(): event.accept() return False else: event.ignore() return True else: return super().eventFilter(watched, event) def show_about_dialog(self): """Show about application dialog.""" box = QtWidgets.QMessageBox() box.setIconPixmap(self.ui.windowIcon().pixmap(64, 64)) box.setText("Tailor") box.setInformativeText( dedent( f""" <p>Version {__version__}.</p> <p>Tailor is written by David Fokkema for use in the physics lab courses at the Vrije Universiteit Amsterdam and the University of Amsterdam.</p> <p>Tailor is free software licensed under the GNU General Public License v3.0 or later.</p> <p>For more information, please visit:<br><a href="https://github.com/davidfokkema/tailor">https://github.com/davidfokkema/tailor</a></p> """ ) ) box.exec() def edit_or_move_down(self): """Edit cell or move cursor down a row. Start editing a cell. If the cell was already being edited, move the cursor down a row, stopping the edit in the process. Trigger a recalculation of all calculated columns. """ cur_index = self.ui.data_view.currentIndex() if not self.ui.data_view.isPersistentEditorOpen(cur_index): # is not yet editing, so start an edit self.ui.data_view.edit(cur_index) else: # is already editing, what index is below? new_index = self.get_index_below_selected_cell() if new_index == cur_index: # already on bottom row, create a new row and take that index self.add_row() new_index = self.get_index_below_selected_cell() # move to it (finishing editing in the process) self.ui.data_view.setCurrentIndex(new_index) def clear_selected_cells(self): """Clear contents of selected cells.""" for index in self.selection.selectedIndexes(): self.data_model.setData(index, "", skip_update=True) # signal that ALL values may have changed using an invalid index # this can be MUCH quicker than emitting signals for each cell self.data_model.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex()) # recalculate computed values once self.data_model.recalculate_all_columns() def get_index_below_selected_cell(self): """Get index directly below the selected cell.""" return self.ui.data_view.moveCursor( self.ui.data_view.MoveDown, QtCore.Qt.NoModifier ) def selection_changed(self, selected, deselected): """Handle selectionChanged events in the data view. When the selection is changed, the column index of the left-most cell in the first selection is used to identify the column name and the mathematical expression that is used to calculate the column values. These values are used to update the column information in the user interface. Args: selected: QItemSelection containing the newly selected events. deselected: QItemSelection containing previously selected, and now deselected, items. """ if not selected.isEmpty(): self.ui.nameLabel.setEnabled(True) self.ui.name_edit.setEnabled(True) first_selection = selected.first() col_idx = first_selection.left() self._selected_col_idx = col_idx self.ui.name_edit.setText(self.data_model.get_column_name(col_idx)) self.ui.formula_edit.setText(self.data_model.get_column_expression(col_idx)) if self.data_model.is_calculated_column(col_idx): self.ui.formulaLabel.setEnabled(True) self.ui.formula_edit.setEnabled(True) else: self.ui.formulaLabel.setEnabled(False) self.ui.formula_edit.setEnabled(False) else: self.ui.nameLabel.setEnabled(False) self.ui.name_edit.clear() self.ui.name_edit.setEnabled(False) self.ui.formulaLabel.setEnabled(False) self.ui.formula_edit.clear() self.ui.formula_edit.setEnabled(False) def tab_changed(self, idx): """Handle currentChanged events of the tab widget. When the tab widget changes to a plot tab, update the plot to reflect any changes to the data that might have occured. Args: idx: an integer index of the now-focused tab. """ self.update_plot_tab(idx) def update_plot_tab(self, idx): """Update plot tab. Update the plot to reflect any changes to the data that might have occured. Args: idx: an integer index of the tab. """ tab = self.ui.tabWidget.widget(idx).code if type(tab) == PlotTab: tab.update_plot() def update_all_plots(self): """Update all plot tabs. Update all plots to reflect any changes to the data that might have occured. """ for idx in range(self.ui.tabWidget.count()): self.update_plot_tab(idx) def add_column(self): """Add column to data model and select it.""" col_index = self.data_model.columnCount() self.data_model.insertColumn(col_index) self.ui.data_view.selectColumn(col_index) self.ui.name_edit.selectAll() self.ui.name_edit.setFocus() def add_calculated_column(self): """Add a calculated column to data model and select it.""" col_index = self.data_model.columnCount() self.data_model.insert_calculated_column(col_index) self.ui.data_view.selectColumn(col_index) self.ui.name_edit.selectAll() self.ui.name_edit.setFocus() def add_row(self): """Add row to data model.""" self.data_model.insertRow(self.data_model.rowCount()) def remove_column(self): """Remove selected column(s) from data model.""" selected_columns = [s.column() for s in self.selection.selectedColumns()] if selected_columns: # Remove columns in reverse order to avoid index shifting during # removal. WIP: It would be more efficient to merge ranges of # contiguous columns since they can be removed in one fell swoop. selected_columns.sort(reverse=True) for column in selected_columns: self.data_model.removeColumn(column) else: error_msg = QtWidgets.QMessageBox() error_msg.setText("You must select one or more columns.") error_msg.exec() def remove_row(self): """Remove selected row(s) from data model.""" selected_rows = [s.row() for s in self.selection.selectedRows()] if selected_rows: # Remove rows in reverse order to avoid index shifting during # removal. WIP: It would be more efficient to merge ranges of # contiguous rows since they can be removed in one fell swoop. selected_rows.sort(reverse=True) for row in selected_rows: self.data_model.removeRow(row) else: error_msg = QtWidgets.QMessageBox() error_msg.setText("You must select one or more rows.") error_msg.exec() def rename_column(self, name): """Rename a column. Renames the currently selected column. Args: name: a QString containing the new name. """ if self._selected_col_idx is not None: # Do not allow empty names or duplicate column names if name and name not in self.data_model.get_column_names(): old_name = self.data_model.get_column_name(self._selected_col_idx) new_name = self.data_model.rename_column(self._selected_col_idx, name) self.rename_plot_variables(old_name, new_name) # set the normalized name to the name edit field self.ui.name_edit.setText(new_name) def rename_plot_variables(self, old_name, new_name): """Rename any plotted variables Args: old_name: the name that may be currently in use. new_name: the new column name """ num_tabs = self.ui.tabWidget.count() tabs = [self.ui.tabWidget.widget(i).code for i in range(num_tabs)] for tab in tabs: if type(tab) == PlotTab: needs_info_update = False for var in ["x_var", "y_var", "x_err_var", "y_err_var"]: if getattr(tab, var) == old_name: needs_info_update = True setattr(tab, var, new_name) # The following creates problems with partial matches # For now, the model function is *not* updated # # if var == "x_var": # update model expression and model object # expr = tab.model_func.text() # new_expr = expr.replace(old_name, new_name) # tab.model_func.setText(new_expr) # tab.get_params_and_update_model() if var == "y_var": # update y-label for model expression tab.update_function_label(new_name) if needs_info_update: tab.update_info_box() def update_column_expression(self, expression): """Update a column expression. Tries to recalculate the values of the currently selected column in the data model. Args: expression: a QString containing the mathematical expression. """ if self._selected_col_idx is not None: self.data_model.update_column_expression(self._selected_col_idx, expression) def ask_and_create_plot_tab(self): """Opens a dialog and create a new tab with a plot. First, a create plot dialog is opened to query the user for the columns to plot. When the dialog is accepted, creates a new tab containing the requested plot. """ dialog = self.create_plot_dialog() if dialog.exec() == QtWidgets.QDialog.Accepted: x_var = dialog.x_axis_box.currentText() y_var = dialog.y_axis_box.currentText() x_err = dialog.x_err_box.currentText() y_err = dialog.y_err_box.currentText() if x_var and y_var: self.create_plot_tab(x_var, y_var, x_err, y_err) def create_plot_tab(self, x_var, y_var, x_err=None, y_err=None): """Create a new tab with a plot. After creating the plot, the tab containing the plot is focused. Args: x_var: the name of the variable to plot on the x-axis. y_var: the name of the variable to plot on the y-axis. x_err: the name of the variable to use for the x-error bars. y_err: the name of the variable to use for the y-error bars. """ plot_tab = PlotTab(self.data_model, main_app=self) idx = self.ui.tabWidget.addTab(plot_tab.ui, f"Plot {self._plot_num}") self._plot_num += 1 plot_tab.create_plot(x_var, y_var, x_err, y_err) self.ui.tabWidget.setCurrentIndex(idx) def create_plot_dialog(self): """Create a dialog to request variables for creating a plot.""" create_dialog = QUiLoader().load( resources.path("tailor.resources", "create_plot_dialog.ui"), self.ui, ) choices = [None] + self.data_model.get_column_names() create_dialog.x_axis_box.addItems(choices) create_dialog.y_axis_box.addItems(choices) create_dialog.x_err_box.addItems(choices) create_dialog.y_err_box.addItems(choices) return create_dialog def close_tab(self, idx): """Close a plot tab. Closes the requested tab, but do not close the table view. Args: idx: an integer tab index """ if idx > 0: # Don't close the table view, only close plot tabs if self.confirm_close_dialog("Are you sure you want to close this plot?"): self.ui.tabWidget.removeTab(idx) def clear_all(self): """Clear all program state. Closes all tabs and data. """ for idx in range(self.ui.tabWidget.count(), 0, -1): # close all plot tabs in reverse order, they are no longer valid self.ui.tabWidget.removeTab(idx) self._plot_num = 1 self.data_model = DataModel(main_app=self) self._set_view_and_selection_model() self.ui.data_view.setCurrentIndex(self.data_model.createIndex(0, 0)) self._set_project_path(None) self.mark_project_dirty(False) def new_project(self): """Close the current project and open a new one.""" if self.confirm_project_close_dialog(): self.clear_all() def save_project_or_dialog(self): """Save project or present a dialog. When you first save a project, present a dialog to select a filename. If you previously opened or saved this project, just save it without presenting the dialog. """ if self._project_filename is None: self.save_as_project_dialog() else: self.save_project(self._project_filename) def save_as_project_dialog(self): """Present save project dialog and save project.""" filename, _ = QtWidgets.QFileDialog.getSaveFileName( parent=self.ui, dir=self.get_recent_directory(), filter="Tailor project files (*.tlr);;All files (*)", ) if filename: self.set_recent_directory(pathlib.Path(filename).parent) self.save_project(filename) def save_project(self, filename): """Save a Tailor project. Save all data and program state (i.e. plot tabs, fit parameters, etc.) to a Tailor project file. Args: filename: a string containing the filename to save to. """ try: save_obj = { "application": __name__, "version": __version__, "data_model": {}, "tabs": [], "plot_num": self._plot_num, "current_tab": self.ui.tabWidget.currentIndex(), } # save data for the data model self.data_model.save_state_to_obj(save_obj["data_model"]) for idx in range(1, self.ui.tabWidget.count()): # save data for each tab tab = self.ui.tabWidget.widget(idx).code tab_data = {"label": self.ui.tabWidget.tabBar().tabText(idx)} tab.save_state_to_obj(tab_data) save_obj["tabs"].append(tab_data) except Exception as exc: self._show_exception( exc, title="Unable to save project.", text="This is a bug in the application.", ) # save data to disk with gzip.open(filename, "w") as f: f.write(json.dumps(save_obj).encode("utf-8")) # remember filename for subsequent call to "Save" self._set_project_path(filename) self.update_recent_files(filename) self.mark_project_dirty(False) def open_project_dialog(self): """Present open project dialog and load project.""" if self.confirm_project_close_dialog(): filename, _ = QtWidgets.QFileDialog.getOpenFileName( parent=self.ui, dir=self.get_recent_directory(), filter="Tailor project files (*.tlr);;All files (*)", ) if filename: self.set_recent_directory(pathlib.Path(filename).parent) self.load_project(filename) def get_recent_directory(self): """Get recent directory from config file. Returns: str: the most recently visited directory. """ cfg = config.read_config() return cfg.get("recent_dir", None) def set_recent_directory(self, directory): """Save the most recently visited directory to the config file. Args: directory (str or pathlib.Path): the most recently visited directory. """ cfg = config.read_config() cfg["recent_dir"] = str(directory) config.write_config(cfg) def confirm_project_close_dialog(self): """Present a confirmation dialog before closing a project. Present a dialog to confirm that the user really wants to close a project and lose possible changes. Returns: A boolean. If True, the user confirms closing the project. If False, the user wants to cancel the action. """ if not self._is_dirty: # There are no changes, skip confirmation dialog return True else: msg = "This action will lose any changes in the current project. Discard the current project, or cancel?" button = QtWidgets.QMessageBox.warning( self.ui, "Please confirm", msg, buttons=QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel, defaultButton=QtWidgets.QMessageBox.Cancel, ) if button == QtWidgets.QMessageBox.Discard: return True elif button == QtWidgets.QMessageBox.Save: self.save_project_or_dialog() return True else: return False def confirm_close_dialog(self, msg=None): """Present a confirmation dialog before closing. Present a dialog to confirm that the user really wants to close an object and lose possible changes. Args: msg: optional message to present to the user. If None, the default message asks for confirmation to discard the current project. Returns: A boolean. If True, the user confirms closing the project. If False, the user wants to cancel the action. """ if msg is None: msg = "You might lose changes." button = QtWidgets.QMessageBox.warning( self.ui, "Please confirm", msg, buttons=QtWidgets.QMessageBox.Close | QtWidgets.QMessageBox.Cancel, defaultButton=QtWidgets.QMessageBox.Cancel, ) if button == QtWidgets.QMessageBox.Close: return True else: return False def load_project(self, filename): """Load a Tailor project. Load all data and program state (i.e. plot tabs, fit parameters, etc.) from a Tailor project file. Args: filename: a string containing the filename to load from. """ try: with gzip.open(filename) as f: save_obj = json.loads(f.read().decode("utf-8")) if save_obj["application"] == __name__: self.clear_all() # remember filename for subsequent call to "Save" self._set_project_path(filename) # load data for the data model self.data_model.load_state_from_obj(save_obj["data_model"]) # create a tab and load data for each plot for tab_data in save_obj["tabs"]: plot_tab = PlotTab(self.data_model, main_app=self) self.ui.tabWidget.addTab(plot_tab.ui, tab_data["label"]) plot_tab.load_state_from_obj(tab_data) self._plot_num = save_obj["plot_num"] self.ui.tabWidget.setCurrentIndex(save_obj["current_tab"]) except Exception as exc: self._show_exception( exc, title="Unable to open project.", text="This can happen if the file is corrupt or if there is a bug in the application.", ) else: self.update_recent_files(filename) self.mark_project_dirty(False) self.ui.statusbar.showMessage( "Finished loading project.", timeout=MSG_TIMEOUT ) def export_csv(self): """Export all data as CSV. Export all data in the table as a comma-separated values file. """ filename, _ = QtWidgets.QFileDialog.getSaveFileName( parent=self.ui, dir=self.get_recent_directory(), filter="CSV files (*.csv);;Text files (*.txt);;All files (*)", ) if filename: self.set_recent_directory(pathlib.Path(filename).parent) self.data_model.write_csv(filename) def import_csv(self): """Import data from a CSV file. After confirmation, erase all data and import from a comma-separated values file. """ if self.confirm_project_close_dialog(): filename, _ = QtWidgets.QFileDialog.getOpenFileName( parent=self.ui, dir=self.get_recent_directory(), filter="CSV files (*.csv);;Text files (*.txt);;All files (*)", ) if filename: self.set_recent_directory(pathlib.Path(filename).parent) dialog = CSVFormatDialog(filename, parent=self.ui) if dialog.ui.exec() == QtWidgets.QDialog.Accepted: ( delimiter, decimal, thousands, header, skiprows, ) = dialog.get_format_parameters() self._do_import_csv( filename, delimiter, decimal, thousands, header, skiprows, ) def _do_import_csv(self, filename, delimiter, decimal, thousands, header, skiprows): """Import CSV data from file. Args: filename: a string containing the path to the CSV file delimiter: a string containing the column delimiter decimal: a string containing the decimal separator thousands: a string containing the thousands separator header: an integer with the row number containing the column names, or None. skiprows: an integer with the number of rows to skip at start of file """ if self.data_model.is_empty(): # when the data only contains empty cells self.clear_all() import_func = self.data_model.read_csv else: import_func = self.data_model.read_and_concat_csv import_func( filename, delimiter=delimiter, decimal=decimal, thousands=thousands, header=header, skiprows=skiprows, ) self.ui.data_view.setCurrentIndex(self.data_model.createIndex(0, 0)) self.update_all_plots() def export_graph(self, suffix): """Export a graph to a file. If the user specifies a name with a different suffix an error will be displayed. Args: suffix: the required suffix of the file. """ tab = self.ui.tabWidget.currentWidget().code if type(tab) == PlotTab: filename, _ = QtWidgets.QFileDialog.getSaveFileName( parent=self.ui, dir=self.get_recent_directory(), filter=f"Graphics (*{suffix});;All files (*)", ) if filename: path = pathlib.Path(filename) self.set_recent_directory(path.parent) if path.suffix == suffix: try: tab.export_graph(path) except Exception as exc: self._show_exception( exc, title="Unable to export graph.", text="This can happen if there is a bug in the application.", ) else: error_msg = QtWidgets.QMessageBox() error_msg.setText(f"You didn't select a {suffix} file.") error_msg.exec() else: error_msg = QtWidgets.QMessageBox() error_msg.setText("You must select a plot tab first.") error_msg.exec() def _set_project_path(self, filename): """Set window title and project name.""" self._project_filename = filename self.update_window_title() def update_window_title(self): """Update window title. Include project name and dirty flag in the title. """ filename = self._project_filename title = "Tailor" if filename is not None: title += f": {pathlib.Path(filename).stem}" if self._is_dirty: title += "*" self.ui.setWindowTitle(title) def _show_exception(self, exc, title, text): """Show a messagebox with detailed exception information. Args: exc: the exception. title: short header text. text: longer informative text describing the problem. """ msg = QtWidgets.QMessageBox(parent=self.ui) msg.setText(title) msg.setInformativeText(text) msg.setDetailedText(traceback.format_exc()) msg.setStyleSheet("QLabel{min-width: 400px;}") msg.exec() def update_recent_files(self, file=None): """Update open recent files list. Update open recent files list in menu and in configuration file. Args: file (pathlib.Path or str): the most recent file which will be added to the list. """ cfg = config.read_config() recents = cfg.get("recent_files", []) if file: path = str(file) if path in recents: recents.remove(path) recents.insert(0, path) recents = recents[:MAX_RECENT_FILES] cfg["recent_files"] = recents config.write_config(cfg) self.populate_recent_files_menu(recents) def populate_recent_files_menu(self, recents): """Populate the open recent files menu. Populate the recent files with a list of recent file names. Args: recents (list): A list of recent file names. """ if self._recent_files_actions: for action in self._recent_files_actions: self.ui.menuOpen_Recent.removeAction(action) if recents: actions = [QtGui.QAction(f) for f in recents] self.ui.menuOpen_Recent.insertActions( self.ui._recent_files_separator, actions ) for action in actions: action.triggered.connect( partial(self.open_recent_project_action, action.text()) ) self.ui.actionClear_Menu.setEnabled(True) self._recent_files_actions = actions def clear_recent_files_menu(self): """Clear the open recent files menu.""" for action in self._recent_files_actions: self.ui.menuOpen_Recent.removeAction(action) self._recent_files_actions = None cfg = config.read_config() cfg["recent_files"] = [] config.write_config(cfg) self.ui.actionClear_Menu.setEnabled(False) def open_recent_project_action(self, filename): if self.confirm_project_close_dialog(): self.load_project(filename) def check_for_updates(self, silent=False): """Check for new releases of Tailor. Args: silent (bool, optional): If there are no updates available, should this method return silently? Defaults to False. """ latest_version, update_link = self.get_latest_version_and_update_link() if latest_version is None: msg = "You appear to have no internet connection or GitHub is down." elif update_link is None: msg = f"You appear to be on the latest version ({__version__}), great!" else: msg = dedent( f"""\ <p>There is a new version available. You have version {__version__} and the latest version is {latest_version}. You can download the new version using the link below.</p> <p><a href={update_link}>Download update.</a></p> """ ) if silent and update_link is None: # no updates, and asked to be silent return else: box = QtWidgets.QMessageBox() box.setText("Updates") box.setInformativeText(msg) box.setStyleSheet("QLabel{min-width: 300px;}") box.exec() def get_latest_version_and_update_link(self): """Get latest version and link to latest release, if available. Get the latest version of Tailor. If a new release is available, returns a platform-specific download link. If there is no new release, returns None. Returns: str: URL to download link or None. """ try: r = urllib.request.urlopen(RELEASE_API_URL, timeout=HTTP_TIMEOUT) except urllib.error.URLError: # no internet connection? return None, None else: release_info = json.loads(r.read()) latest_version = release_info["name"] update_link = None if packaging.version.parse(latest_version) > packaging.version.parse( __version__ ): urls = { pathlib.Path(a["name"]).suffix: a["browser_download_url"] for a in release_info["assets"] } system = platform.system() try: if system == "Darwin": update_link = urls[".dmg"] elif system == "Windows": update_link = urls[".msi"] except KeyError: # installer not available, no update link pass return latest_version, update_link
if __name__ == "__main__": import sys QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) app = QApplication(sys.argv) ui_file_name = "calculator_layout.ui" ui_file = QFile(ui_file_name) if not ui_file.open(QIODevice.ReadOnly): print("Cannot open {}: {}".format(ui_file_name, ui_file.errorString())) sys.exit(-1) loader = QUiLoader() dialog = loader.load(ui_file) ui_file.close() if not dialog: print(loader.errorString()) sys.exit(-1) #loader.load(ui_file).show() dialog.show() sys.exit(app.exec()) """ app = QtWidgets.QApplication(sys.argv) Dialog = QtWidgets.QDialog()
def __init__(self): """""" super().__init__() self.main = QUiLoader().load('main_window.ui', self) self.main.pushButton_2.setProperty('class', 'big_button')