def data(self, index, role=QtCore.Qt.DisplayRole): """Return a data field at the given index (of type QModelIndex, specifying row and column); overrides the corresponding QAbstractTableModel method. NOTE: Other roles (e.g. for display appearance) could be specified in this method as well. Cf. the 'ships' example in chapter 14/16 of 'Rapid GUI Programming with Python and Qt: The Definitive Guide to PyQt Programming' (Mark Summerfield). """ waypoints = self.waypoints if not index.isValid() or not (0 <= index.row() < len(waypoints)): return QtCore.QVariant() waypoint = waypoints[index.row()] column = index.column() if role == QtCore.Qt.DisplayRole: if self.performance_settings["visible"]: return QtCore.QVariant(TABLE_FULL[column][1](waypoint)) else: return QtCore.QVariant(TABLE_SHORT[column][1](waypoint)) elif role == QtCore.Qt.TextAlignmentRole: return QtCore.QVariant( int(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)) return QtCore.QVariant()
def setModelData(self, editor, model, index): """For the LOCATION column: If the user selects a location from the combobox, get the corresponding coordinates. """ if index.column() == LOCATION: loc = editor.currentText() locations = config_loader(dataset='locations', default=mss_default.locations) if loc in locations: lat, lon = locations[loc] # Don't update distances and flight performance twice, hence # set update=False for LAT. model.setData(index.sibling(index.row(), LAT), QtCore.QVariant(lat), update=False) model.setData(index.sibling(index.row(), LON), QtCore.QVariant(lon)) else: for wp in self.parent().waypoints_model.all_waypoint_data(): if loc == wp.location: lat, lon = wp.lat, wp.lon # Don't update distances and flight performance twice, hence # set update=False for LAT. model.setData(index.sibling(index.row(), LAT), QtCore.QVariant(lat), update=False) model.setData(index.sibling(index.row(), LON), QtCore.QVariant(lon)) model.setData(index, QtCore.QVariant(editor.currentText())) else: QtWidgets.QItemDelegate.setModelData(self, editor, model, index)
def test_mouse_over(self, mockbox): # Test mouse over QtTest.QTest.mouseMove(self.window.mpl.canvas, QtCore.QPoint(782, 266), -1) QtWidgets.QApplication.processEvents() QtTest.QTest.mouseMove(self.window.mpl.canvas, QtCore.QPoint(20, 20), -1) QtWidgets.QApplication.processEvents()
def __init__(self, canvas, parent, sideview=False, coordinates=True): self.sideview = sideview if sideview: self.toolitems = [ _x for _x in self.toolitems if _x[0] in ('Save', ) ] self.set_history_buttons = lambda: None else: self.toolitems = [ _x for _x in self.toolitems if _x[0] in (None, 'Home', 'Back', 'Forward', 'Pan', 'Zoom', 'Save') ] self.toolitems.extend([ (None, None, None, None), ('Mv WP', 'Move waypoints', "wp_move", 'move_wp'), ('Ins WP', 'Insert waypoints', "wp_insert", 'insert_wp'), ('Del WP', 'Delete waypoints', "wp_delete", 'delete_wp'), ]) super(NavigationToolbar, self).__init__(canvas, parent, coordinates) self._actions["move_wp"].setCheckable(True) self._actions["insert_wp"].setCheckable(True) self._actions["delete_wp"].setCheckable(True) self.setIconSize(QtCore.QSize(24, 24)) self.layout().setSpacing(12) self.canvas = canvas
def display_uploaded_img(self, file_path): self.messageText.clear() image_uri = QtCore.QUrl(f"file://{file_path}") image = QtGui.QImage(QtGui.QImageReader(file_path).read()) self.messageText.document().addResource( QtGui.QTextDocument.ImageResource, image_uri, QtCore.QVariant(image)) img_width, img_height = self.get_img_dimensions(image) image_format = QtGui.QTextImageFormat() image_format.setWidth(img_width) image_format.setHeight(img_height) image_format.setName(image_uri.toString()) cursor = self.messageText.textCursor() cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.MoveAnchor) cursor.insertImage(image_format) self.messageText.setReadOnly(True)
def removeRows(self, position, rows=1, index=QtCore.QModelIndex()): """Remove waypoint; overrides the corresponding QAbstractTableModel method. """ # beginRemoveRows emits rowsAboutToBeRemoved(index, first, last). self.beginRemoveRows(QtCore.QModelIndex(), position, position + rows - 1) self.waypoints = self.waypoints[:position] + self.waypoints[position + rows:] if position < len(self.waypoints): self.update_distances(position, rows=min(rows, len(self.waypoints) - position)) # endRemoveRows emits rowsRemoved(index, first, last). self.endRemoveRows() self.modified = True return True
def button_release_move_callback(self, event): """Called whenever a mouse button is released. """ if not self.showverts or event.button != 1 or self._ind is None: return # Submit the new position to the data model. vertices = self.pathpatch.get_path().wp_vertices lon, lat = self.map(vertices[self._ind][0], vertices[self._ind][1], inverse=True) loc = find_location(lat, lon, tolerance=self.appropriate_epsilon_km(px=15)) if loc is not None: lat, lon = loc[0] self.waypoints_model.setData( self.waypoints_model.createIndex(self._ind, ft.LAT), QtCore.QVariant(lat), update=False) self.waypoints_model.setData( self.waypoints_model.createIndex(self._ind, ft.LON), QtCore.QVariant(lon)) self._ind = None
class MscolabHelpDialog(QtWidgets.QDialog, msc_help_dialog.Ui_mscolabHelpDialog): viewCloses = QtCore.pyqtSignal(name="viewCloses") def __init__(self, parent=None): super(MscolabHelpDialog, self).__init__(parent) self.setupUi(self) self.okayBtn.clicked.connect(lambda: self.close()) def closeEvent(self, event): self.viewCloses.emit()
def insertRows(self, position, rows=1, index=QtCore.QModelIndex(), waypoints=None): """Insert waypoint; overrides the corresponding QAbstractTableModel method. """ if not waypoints: waypoints = [Waypoint(0, 0, 0)] * rows assert len(waypoints) == rows, (waypoints, rows) self.beginInsertRows(QtCore.QModelIndex(), position, position + rows - 1) for row, wp in enumerate(waypoints): self.waypoints.insert(position + row, wp) self.update_distances(position, rows=rows) self.endInsertRows() self.modified = True return True
def button_release_move_callback(self, event): """Called whenever a mouse button is released. """ if not self.showverts or event.button != 1: return if self._ind is not None: # Submit the new pressure (the only value that can be edited # in the side view) to the data model. vertices = self.pathpatch.get_path().vertices pressure = vertices[self._ind][1] # http://doc.trolltech.com/4.3/qabstractitemmodel.html#createIndex qt_index = self.waypoints_model.createIndex(self._ind, ft.PRESSURE) # NOTE: QVariant cannot handle numpy.float64 types, hence convert # to float(). self.waypoints_model.setData(qt_index, QtCore.QVariant(float(pressure / 100.))) self._ind = None
def test_insert_point(self, mockbox): """ Test inserting a point inside and outside the canvas """ self.window.mpl.navbar._actions['insert_wp'].trigger() QtWidgets.QApplication.processEvents() assert len(self.window.waypoints_model.waypoints) == 3 QtTest.QTest.mouseClick(self.window.mpl.canvas, QtCore.Qt.LeftButton) QtWidgets.QApplication.processEvents() assert len(self.window.waypoints_model.waypoints) == 4 QtTest.QTest.mouseClick(self.window.mpl.canvas, QtCore.Qt.LeftButton, pos=QtCore.QPoint(1, 1)) QtWidgets.QApplication.processEvents() assert len(self.window.waypoints_model.waypoints) == 4 QtTest.QTest.mouseClick(self.window.mpl.canvas, QtCore.Qt.LeftButton) # click again on same position QtWidgets.QApplication.processEvents() assert len(self.window.waypoints_model.waypoints) == 5 assert mockbox.critical.call_count == 0
def test_move_point(self, mockbox): self.window.mpl.navbar._actions['insert_wp'].trigger() QtWidgets.QApplication.processEvents() assert len(self.window.waypoints_model.waypoints) == 3 QtWidgets.QApplication.processEvents() QtTest.QTest.mouseClick(self.window.mpl.canvas, QtCore.Qt.LeftButton) QtWidgets.QApplication.processEvents() assert len(self.window.waypoints_model.waypoints) == 4 self.window.mpl.navbar._actions['move_wp'].trigger() QtWidgets.QApplication.processEvents() QtTest.QTest.mousePress(self.window.mpl.canvas, QtCore.Qt.LeftButton) QtWidgets.QApplication.processEvents() point = QtCore.QPoint((self.window.width() // 3), self.window.height() // 2) QtTest.QTest.mouseMove(self.window.mpl.canvas, pos=point) QtWidgets.QApplication.processEvents() QtTest.QTest.mouseRelease(self.window.mpl.canvas, QtCore.Qt.LeftButton, pos=point) QtWidgets.QApplication.processEvents() assert len(self.window.waypoints_model.waypoints) == 4 assert mockbox.critical.call_count == 0
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): """Return data describing the table header; overrides the corresponding QAbstractTableModel method. """ if role == QtCore.Qt.TextAlignmentRole: if orientation == QtCore.Qt.Horizontal: return QtCore.QVariant( int(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)) return QtCore.QVariant( int(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)) if role != QtCore.Qt.DisplayRole: return QtCore.QVariant() # Return the names of the table columns. if orientation == QtCore.Qt.Horizontal: if self.performance_settings["visible"]: return QtCore.QVariant(TABLE_FULL[section][0]) else: return QtCore.QVariant(TABLE_SHORT[section][0]) # Table rows (waypoints) are labelled with their number (= number of # waypoint). return QtCore.QVariant(int(section))
def show_online_help(self): """Open Documentation in a browser""" QtGui.QDesktopServices.openUrl( QtCore.QUrl("http://mss.readthedocs.io/en/stable"))
def columnCount(self, index=QtCore.QModelIndex()): return len(TABLE_FULL)
def rowCount(self, index=QtCore.QModelIndex()): """Number of waypoints in the model. """ return len(self.waypoints)
class VPathInteractor(PathInteractor): """Subclass of PathInteractor that implements an interactively editable vertical profile of the flight track. """ signal_get_vsec = QtCore.Signal(name="get_vsec") def __init__(self, ax, waypoints, redraw_xaxis=None, clear_figure=None, numintpoints=101): """Constructor passes a PathV instance its parent. Arguments: ax -- matplotlib.Axes object into which the path should be drawn. waypoints -- flighttrack.WaypointsModel instance. numintpoints -- number of intermediate interpolation points. The entire flight track will be interpolated to this number of points. redrawXAxis -- callback function to redraw the x-axis on path changes. """ self.numintpoints = numintpoints self.redraw_xaxis = redraw_xaxis self.clear_figure = clear_figure super(VPathInteractor, self).__init__( ax=ax, waypoints=waypoints, mplpath=PathV([[0, 0]], numintpoints=numintpoints)) def get_num_interpolation_points(self): return self.numintpoints def redraw_figure(self): """For the side view, changes in the horizontal position of a waypoint (including moved waypoints, new or deleted waypoints) make a complete redraw of the figure necessary. Calls the callback function 'redrawXAxis()'. """ self.redraw_path() # emit signal to redraw map self.signal_get_vsec.emit() if self.clear_figure() is not None: self.clear_figure() if self.redraw_xaxis is not None: self.redraw_xaxis(self.path.ilats, self.path.ilons, self.path.itimes) self.ax.figure.canvas.draw() def button_release_delete_callback(self, event): """Called whenever a mouse button is released. """ if not self.showverts or event.button != 1: return if self._ind is not None: if self.confirm_delete_waypoint(self._ind): # removeRows() will trigger a signal that will redraw the path. self.waypoints_model.removeRows(self._ind) self._ind = None def button_release_insert_callback(self, event): """Called whenever a mouse button is released. From the click event's coordinates, best_index is calculated as the index of a vertex whose x coordinate > clicked x coordinate. This is the position where the waypoint is to be inserted. 'lat' and 'lon' are calculated as an average of each of the first waypoint in left and right neighbourhood of inserted waypoint. The coordinates are checked against "locations" defined in mss' config. A new waypoint with the coordinates, and name is inserted into the waypoints_model. """ if not self.showverts or event.button != 1 or event.inaxes is None: return y = event.ydata wpm = self.waypoints_model flightlevel = float(pressure2flightlevel(y)) [lat, lon], best_index = self.get_lat_lon(event) loc = find_location(lat, lon) # skipped tolerance which uses appropriate_epsilon_km if loc is not None: (lat, lon), location = loc else: location = "" new_wp = ft.Waypoint(lat, lon, flightlevel, location=location) wpm.insertRows(best_index, rows=1, waypoints=[new_wp]) self.redraw_figure() self._ind = None def get_lat_lon(self, event): x = event.xdata wpm = self.waypoints_model vertices = self.pathpatch.get_path().vertices vertices = np.ndarray.tolist(vertices) for index, vertex in enumerate(vertices): vertices[index].append(datetime.datetime(2012, 7, 1, 10, 30)) best_index = 1 # if x axis has increasing coordinates if vertices[-1][0] > vertices[0][0]: for index, vertex in enumerate(vertices): if x >= vertex[0]: best_index = index + 1 # if x axis has decreasing coordinates else: for index, vertex in enumerate(vertices): if x <= vertex[0]: best_index = index + 1 # number of subcoordinates is determined by difference in x coordinates number_of_intermediate_points = math.floor(vertices[best_index][0] - vertices[best_index - 1][0]) intermediate_vertices_list = path_points([vertices[best_index - 1], vertices[best_index]], number_of_intermediate_points) wp1Array = [wpm.waypoint_data(best_index - 1).lat, wpm.waypoint_data(best_index - 1).lon, datetime.datetime(2012, 7, 1, 10, 30)] wp2Array = [wpm.waypoint_data(best_index).lat, wpm.waypoint_data(best_index).lon, datetime.datetime(2012, 7, 1, 10, 30)] intermediate_waypoints_list = latlon_points(wp1Array, wp2Array, number_of_intermediate_points, connection="greatcircle") # best_index1 is the best index among the intermediate coordinates to fit the hovered point # if x axis has increasing coordinates best_index1 = 1 if vertices[-1][0] > vertices[0][0]: for index, vertex in enumerate(intermediate_vertices_list[0]): if x >= vertex: best_index1 = index + 1 # if x axis has decreasing coordinates else: for index, vertex in enumerate(intermediate_vertices_list[0]): if x <= vertex: best_index1 = index + 1 # depends if best_index1 or best_index1 - 1 on closeness to left or right neighbourhood return [intermediate_waypoints_list[0][best_index1 - 1], intermediate_waypoints_list[1][best_index1 - 1]], best_index def button_release_move_callback(self, event): """Called whenever a mouse button is released. """ if not self.showverts or event.button != 1: return if self._ind is not None: # Submit the new pressure (the only value that can be edited # in the side view) to the data model. vertices = self.pathpatch.get_path().vertices pressure = vertices[self._ind][1] # http://doc.trolltech.com/4.3/qabstractitemmodel.html#createIndex qt_index = self.waypoints_model.createIndex(self._ind, ft.PRESSURE) # NOTE: QVariant cannot handle numpy.float64 types, hence convert # to float(). self.waypoints_model.setData(qt_index, QtCore.QVariant(float(pressure / 100.))) self._ind = None def motion_notify_callback(self, event): """Called on mouse movement. Redraws the path if a vertex has been picked and is being dragged. In the side view, the horizontal position of a waypoint is locked. Hence, points can only be moved in the vertical direction (y position in this view). """ if not self.showverts or self._ind is None or event.inaxes is None or event.button != 1: return vertices = self.pathpatch.get_path().vertices # Set the new y position of the vertex to event.ydata. Keep the # x coordinate. vertices[self._ind] = vertices[self._ind][0], event.ydata self.redraw_path(vertices) def qt_data_changed_listener(self, index1, index2): """Listens to dataChanged() signals emitted by the flight track data model. The side view can thus react to data changes induced by another view (table, top view). """ # If the altitude of a point has changed, only the plotted flight # profile needs to be redrawn (redraw_path()). If the horizontal # position of a waypoint has changed, the entire figure needs to be # redrawn, as this affects the x-position of all points. self.pathpatch.get_path().update_from_WaypointsTableModel(self.waypoints_model) if index1.column() in [ft.FLIGHTLEVEL, ft.PRESSURE, ft.LOCATION]: self.redraw_path(self.pathpatch.get_path().vertices) elif index1.column() in [ft.LAT, ft.LON]: self.redraw_figure() elif index1.column() in [ft.TIME_UTC]: if self.redraw_xaxis is not None: self.redraw_xaxis(self.path.ilats, self.path.ilons, self.path.itimes)
def seconds_to_string(seconds): """Format a time given in seconds to a string HH:MM:SS. Used for the 'leg time/cum. time' columns of the table view. """ hours, seconds = divmod(int(seconds), 3600) minutes, seconds = divmod(seconds, 60) return "{:02d}:{:02d}:{:02d}".format(hours, minutes, seconds) TABLE_FULL = [ ("Location ", lambda waypoint: waypoint.location, True), ("Lat\n(+-90)", lambda waypoint: round(float(waypoint.lat), 2), True), ("Lon\n(+-180)", lambda waypoint: round(float(waypoint.lon), 2), True), ("Flightlevel", lambda waypoint: waypoint.flightlevel, True), ("Pressure\n(hPa)", lambda waypoint: QtCore.QLocale().toString( waypoint.pressure / 100., 'f', 2), True), ("Leg dist.\n(km [nm])", lambda waypoint: "{:d} [{:d}]".format( int(waypoint.distance_to_prev), int(waypoint.distance_to_prev / 1.852) ), False), ("Cum. dist.\n(km [nm])", lambda waypoint: "{:d} [{:d}]".format( int(waypoint.distance_total), int(waypoint.distance_total / 1.852)), False), ("Leg time", lambda waypoint: seconds_to_string(waypoint.leg_time), False), ("Cum. time", lambda waypoint: seconds_to_string(waypoint.cum_time), False), ("Time (UTC)", lambda waypoint: waypoint.utc_time.strftime("%Y-%m-%d %H:%M:%S"), False), ("Rem. fuel\n(lb)", lambda waypoint: ("{:d}".format(int(waypoint.rem_fuel))), False), ("Aircraft\nweight (lb)", lambda waypoint: ("{:d}".format(int(waypoint.weight))), False),
class MSColabProjectWindow(QtWidgets.QMainWindow, ui.Ui_MscolabProject): """Derives QMainWindow to provide some common functionality to all MSUI view windows. """ name = "MSColab Project Window" identifier = None viewCloses = QtCore.pyqtSignal(name="viewCloses") reloadWindows = QtCore.pyqtSignal(name="reloadWindows") def __init__(self, token, p_id, user, project_name, access_level, conn, parent=None, mscolab_server_url=config_loader( dataset="default_MSCOLAB", default=mss_default.default_MSCOLAB)): """ token: access_token p_id: project id user: logged in user project_name: active project name, access_level: access level of user logged in conn: to send messages, recv messages, if a direct slot-signal can't be setup to be connected at parents' parent: widget parent mscolab_server_url: server url for mscolab """ super(MSColabProjectWindow, self).__init__(parent) self.setupUi(self) self.mscolab_server_url = mscolab_server_url self.token = token self.user = user self.p_id = p_id self.project_name = project_name self.conn = conn self.access_level = access_level self.text = "" self.attachment = None self.attachment_type = None self.active_edit_id = None self.active_message_reply = None self.current_search_index = None self.markdown = Markdown( extensions=['nl2br', 'sane_lists', DeregisterSyntax()]) self.messageText = MessageTextEdit(self.centralwidget) self.setup_message_text() # Signals self.searchMessageLineEdit.textChanged.connect( self.handle_search_text_changed) self.searchPrevBtn.clicked.connect(self.handle_prev_message_search) self.searchNextBtn.clicked.connect(self.handle_next_message_search) self.previewBtn.clicked.connect(self.toggle_preview) self.sendMessageBtn.clicked.connect(self.send_message) self.uploadBtn.clicked.connect(self.handle_upload) self.editMessageBtn.clicked.connect(self.edit_message) self.cancelBtn.clicked.connect(self.send_message_state) # Socket Connection handlers self.conn.signal_project_permissions_updated.connect( self.handle_permissions_updated) self.conn.signal_message_receive.connect(self.handle_incoming_message) self.conn.signal_message_reply_receive.connect( self.handle_incoming_message_reply) self.conn.signal_message_edited.connect(self.handle_message_edited) self.conn.signal_message_deleted.connect(self.handle_deleted_message) # Set Label text self.set_label_text() # Hide Edit Message section self.send_message_state() # load all users self.load_users() # load messages self.load_all_messages() # UI SET UP METHODS def setup_message_text(self): self.messageText.setAcceptRichText(False) self.messageText.setTextInteractionFlags( QtCore.Qt.LinksAccessibleByKeyboard | QtCore.Qt.LinksAccessibleByMouse | QtCore.Qt.TextBrowserInteraction | QtCore.Qt.TextEditable | QtCore.Qt.TextEditorInteraction | QtCore.Qt.TextSelectableByKeyboard | QtCore.Qt.TextSelectableByMouse) self.messageText.setPlaceholderText( "Enter message here.\nPress enter to send.\nShift+Enter to add a new line." ) self.messageText.setObjectName("messageText") vbox_layout = QtWidgets.QVBoxLayout() vbox_layout.addWidget(self.messageText) vbox_layout.setSpacing(0) vbox_layout.setContentsMargins(0, 0, 0, 0) self.messageTextContainer.setLayout(vbox_layout) def set_label_text(self): self.user_info.setText(f"Logged in: {self.user['username']}") self.proj_info.setText(f"Project: {self.project_name}") def get_img_dimensions(self, image): # TODO: CHECK WHY SCROLL BAR COMES? max_height = self.messageText.size().height() height = max_height - 4 width = image.width() * height / image.height() return width, height def display_uploaded_img(self, file_path): self.messageText.clear() image_uri = QtCore.QUrl(f"file://{file_path}") image = QtGui.QImage(QtGui.QImageReader(file_path).read()) self.messageText.document().addResource( QtGui.QTextDocument.ImageResource, image_uri, QtCore.QVariant(image)) img_width, img_height = self.get_img_dimensions(image) image_format = QtGui.QTextImageFormat() image_format.setWidth(img_width) image_format.setHeight(img_height) image_format.setName(image_uri.toString()) cursor = self.messageText.textCursor() cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.MoveAnchor) cursor.insertImage(image_format) self.messageText.setReadOnly(True) def display_uploaded_document(self, file_path): self.messageText.clear() self.messageText.setText(f"File Selected: {file_path}") self.messageText.setReadOnly(True) # Signal Slots def handle_search_text_changed(self): self.current_search_index = None def handle_prev_message_search(self): if self.current_search_index is None: self.handle_message_search(self.messageList.count() - 1, -1, -1) else: self.handle_message_search(self.current_search_index - 1, -1, -1) def handle_next_message_search(self): if self.current_search_index is None: self.handle_message_search(self.messageList.count() - 1, -1, -1) else: self.handle_message_search(self.current_search_index + 1, self.messageList.count()) def handle_message_search(self, start_index, end_index, step=1): text = self.searchMessageLineEdit.text() if text == "": return for row in range(start_index, end_index, step): item = self.messageList.item(row) message_widget = self.messageList.itemWidget(item) if message_widget.message_type in (MessageType.TEXT, MessageType.DOCUMENT): if text.lower() in message_widget.message_text.lower(): self.messageList.scrollToItem( item, QtWidgets.QAbstractItemView.PositionAtCenter) item.setSelected(True) self.current_search_index = row return if self.current_search_index is None: show_popup(self, "Alert", "No message found!", 1) def toggle_preview(self): # Go Back to text box if self.messageText.isReadOnly(): self.messageText.setHtml("") self.messageText.setText(self.text) self.messageText.moveCursor(QtGui.QTextCursor.End) self.messageText.setReadOnly(False) self.messageText.setStyleSheet("") self.previewBtn.setDefault(False) self.previewBtn.setText("Preview") # Show preview else: self.text = self.messageText.toPlainText() html = self.markdown.convert(self.text) self.messageText.setHtml(html) self.messageText.setReadOnly(True) self.messageText.setStyleSheet("background: #eff0f1") self.previewBtn.setDefault(True) self.previewBtn.setText("Write") def handle_upload(self): img_type = "Image (*.png *.gif *.jpg *jpeg *.bmp)" doc_type = "Document (*.*)" file_filter = f'{img_type};;{doc_type}' file_path, file_type = QtWidgets.QFileDialog.getOpenFileName( self, "Select a file", "", file_filter) if file_path == "": return self.attachment = file_path if file_type == img_type: self.attachment_type = MessageType.IMAGE self.display_uploaded_img(file_path) else: self.attachment_type = MessageType.DOCUMENT self.display_uploaded_document(file_path) self.uploadBtn.setVisible(False) self.cancelBtn.setVisible(True) self.previewBtn.setVisible(False) def send_message(self): """ send message through connection """ if self.attachment is None: reply_id = -1 if self.active_message_reply is not None: reply_id = self.active_message_reply.id message_text = self.messageText.toPlainText() if message_text == "": return message_text = message_text.strip() self.conn.send_message(message_text, self.p_id, reply_id) else: files = {"file": open(self.attachment, 'rb')} data = { "token": self.token, "p_id": self.p_id, "message_type": int(self.attachment_type) } url = url_join(self.mscolab_server_url, 'message_attachment') try: requests.post(url, data=data, files=files) except requests.exceptions.ConnectionError: show_popup(self, "Error", "File size too large") self.send_message_state() def start_message_reply(self, message_item): self.send_message_state() self.active_message_reply = message_item self.active_message_reply.set_selected(True) self.uploadBtn.setVisible(False) self.cancelBtn.setVisible(True) def start_message_edit(self, message_text, message_id): self.send_message_state() self.active_edit_id = message_id self.messageText.setText(message_text) self.messageText.setFocus() self.messageText.moveCursor(Qt.QTextCursor.End) self.editMessageBtn.setVisible(True) self.cancelBtn.setVisible(True) self.sendMessageBtn.setVisible(False) self.uploadBtn.setVisible(False) def send_message_state(self): self.active_edit_id = None self.attachment = None if self.active_message_reply is not None: self.active_message_reply.set_selected(False) self.active_message_reply = None self.messageText.clear() self.messageText.setReadOnly(False) self.messageText.setFocus() self.editMessageBtn.setVisible(False) self.cancelBtn.setVisible(False) self.sendMessageBtn.setVisible(True) self.previewBtn.setVisible(True) self.uploadBtn.setVisible(True) def edit_message(self): new_message_text = self.messageText.toPlainText() if new_message_text == "": self.conn.delete_message(self.active_edit_id, self.p_id) else: new_message_text = new_message_text.strip() self.conn.edit_message(self.active_edit_id, new_message_text, self.p_id) self.send_message_state() # API REQUESTS def load_users(self): # load users to side-tab here # make request to get users data = {"token": self.token, "p_id": self.p_id} url = url_join(self.mscolab_server_url, 'authorized_users') r = requests.get(url, data=data) if r.text == "False": show_popup(self, "Error", "Some error occurred while fetching users!") else: self.collaboratorsList.clear() users = r.json()["users"] for user in users: item = QtWidgets.QListWidgetItem( f'{user["username"]} - {user["access_level"]}', parent=self.collaboratorsList) self.collaboratorsList.addItem(item) def load_all_messages(self): # empty messages and reload from server data = { "token": self.token, "p_id": self.p_id, "timestamp": datetime.datetime(1970, 1, 1).strftime("%Y-%m-%d, %H:%M:%S") } # returns an array of messages url = url_join(self.mscolab_server_url, "messages") res = requests.get(url, data=data).json() messages = res["messages"] # clear message box for message in messages: self.render_new_message(message, scroll=False) self.messageList.scrollToBottom() def render_new_message(self, message, scroll=True): message_item = MessageItem(message, self) list_widget_item = QtWidgets.QListWidgetItem(self.messageList) list_widget_item.setSizeHint(message_item.sizeHint()) self.messageList.addItem(list_widget_item) self.messageList.setItemWidget(list_widget_item, message_item) if scroll: self.messageList.scrollToBottom() # SOCKET HANDLERS @QtCore.Slot(int) def handle_permissions_updated(self, _): self.load_users() @QtCore.Slot(str) def handle_incoming_message(self, message): message = json.loads(message) self.render_new_message(message) @QtCore.Slot(str) def handle_incoming_message_reply(self, reply): reply = json.loads(reply) for i in range(self.messageList.count() - 1, -1, -1): item = self.messageList.item(i) message_widget = self.messageList.itemWidget(item) if message_widget.id == reply["reply_id"]: # TODO: Hacky Approach. Add UI update function in the widget later instead of creating a new widget message_widget.replies.append(reply) message = { "id": message_widget.id, "u_id": message_widget.u_id, "username": message_widget.username, "replies": message_widget.replies, "message_type": message_widget.message_type, "time": message_widget.time } if message_widget.message_type in (MessageType.TEXT, MessageType.SYSTEM_MESSAGE): message["text"] = message_widget.message_text else: message["text"] = message_widget.attachment_path new_message_item = MessageItem(message, self) item.setSizeHint(new_message_item.sizeHint()) self.messageList.setItemWidget(item, new_message_item) break @QtCore.Slot(str) def handle_message_edited(self, message): message = json.loads(message) message_id = message["message_id"] new_message_text = message["new_message_text"] # Loop backwards because it's more likely the message is new than old for i in range(self.messageList.count() - 1, -1, -1): item = self.messageList.item(i) message_widget = self.messageList.itemWidget(item) if message_widget.id == message_id: message_widget.update_text(new_message_text) item.setSizeHint(message_widget.sizeHint()) break @QtCore.Slot(str) def handle_deleted_message(self, message): message = json.loads(message) message_id = message["message_id"] # Loop backwards because it's more likely the message is new than old for i in range(self.messageList.count() - 1, -1, -1): item = self.messageList.item(i) message_widget = self.messageList.itemWidget(item) if message_widget.id == message_id: self.messageList.takeItem(i) break def closeEvent(self, event): self.viewCloses.emit()
def __init__(self, parent=None): super(EditorMainWindow, self).__init__(parent) self.path = None self.file_content = None self.layout = QtWidgets.QVBoxLayout() # Could also use a QTextEdit and set self.editor.setAcceptRichText(False) self.editor = QtWidgets.QPlainTextEdit() # Setup the QTextEdit editor configuration fixedfont = QtGui.QFontDatabase.systemFont( QtGui.QFontDatabase.FixedFont) fixedfont.setPointSize(12) self.editor.setFont(fixedfont) # self.path holds the path of the currently open file. # If none, we haven't got a file open yet (or creating new). self.path = constants.MSS_CONFIG_PATH self.layout.addWidget(self.editor) self.container = QtWidgets.QWidget() self.container.setLayout(self.layout) self.setCentralWidget(self.container) self.status = QtWidgets.QStatusBar() self.setStatusBar(self.status) self.file_toolbar = QtWidgets.QToolBar("File") self.file_toolbar.setIconSize(QtCore.QSize(14, 14)) self.addToolBar(self.file_toolbar) self.file_menu = self.menuBar().addMenu("&File") self.open_file_action = QtWidgets.QAction( QtGui.QIcon(icons('config_editor', 'Folder-new.svg')), "Open file...", self) self.open_file_action.setStatusTip("Open file") self.open_file_action.triggered.connect(self.file_open) self.file_menu.addAction(self.open_file_action) self.file_toolbar.addAction(self.open_file_action) self.save_file_action = QtWidgets.QAction( QtGui.QIcon(icons('config_editor', 'Document-save.svg')), "Save", self) self.save_file_action.setStatusTip("Save current page") self.save_file_action.triggered.connect(self.file_save) self.file_menu.addAction(self.save_file_action) self.file_toolbar.addAction(self.save_file_action) self.saveas_file_action = QtWidgets.QAction( QtGui.QIcon(icons('config_editor', 'Document-save-as.svg')), "Save As...", self) self.saveas_file_action.setStatusTip( "Save current page to specified file") self.saveas_file_action.triggered.connect(self.file_saveas) self.file_menu.addAction(self.saveas_file_action) self.file_toolbar.addAction(self.saveas_file_action) self.print_action = QtWidgets.QAction( QtGui.QIcon(icons('config_editor', 'Document-print.svg')), "Print...", self) self.print_action.setStatusTip("Print current page") self.print_action.triggered.connect(self.file_print) self.file_menu.addAction(self.print_action) self.file_toolbar.addAction(self.print_action) self.edit_toolbar = QtWidgets.QToolBar("Edit") self.edit_toolbar.setIconSize(QtCore.QSize(16, 16)) self.addToolBar(self.edit_toolbar) self.edit_menu = self.menuBar().addMenu("&Edit") self.undo_action = QtWidgets.QAction( QtGui.QIcon(icons('config_editor', 'Edit-undo.svg')), "Undo", self) self.undo_action.setStatusTip("Undo last change") self.undo_action.triggered.connect(self.editor.undo) self.edit_menu.addAction(self.undo_action) self.redo_action = QtWidgets.QAction( QtGui.QIcon(icons('config_editor', 'Edit-redo.svg')), "Redo", self) self.redo_action.setStatusTip("Redo last change") self.redo_action.triggered.connect(self.editor.redo) self.edit_toolbar.addAction(self.redo_action) self.edit_menu.addAction(self.redo_action) self.edit_menu.addSeparator() self.cut_action = QtWidgets.QAction( QtGui.QIcon(icons('config_editor', 'Edit-cut.svg')), "Cut", self) self.cut_action.setStatusTip("Cut selected text") self.cut_action.triggered.connect(self.editor.cut) self.edit_toolbar.addAction(self.cut_action) self.edit_menu.addAction(self.cut_action) self.copy_action = QtWidgets.QAction( QtGui.QIcon(icons('config_editor', 'Edit-copy.svg')), "Copy", self) self.copy_action.setStatusTip("Copy selected text") self.copy_action.triggered.connect(self.editor.copy) self.edit_toolbar.addAction(self.copy_action) self.edit_menu.addAction(self.copy_action) self.paste_action = QtWidgets.QAction( QtGui.QIcon(icons('config_editor', 'Edit-paste.svg')), "Paste", self) self.paste_action.setStatusTip("Paste from clipboard") self.paste_action.triggered.connect(self.editor.paste) self.edit_toolbar.addAction(self.paste_action) self.edit_menu.addAction(self.paste_action) self.select_action = QtWidgets.QAction( QtGui.QIcon(icons('config_editor', 'Edit-select-all.svg')), "Select all", self) self.select_action.setStatusTip("Select all text") self.select_action.triggered.connect(self.editor.selectAll) self.edit_menu.addAction(self.select_action) self.edit_menu.addSeparator() self.wrap_action = QtWidgets.QAction( QtGui.QIcon(icons('config_editor', 'Go-next.svg')), "Wrap text to window", self) self.wrap_action.setStatusTip("Toggle wrap text to window") self.wrap_action.setCheckable(True) self.wrap_action.setChecked(True) self.wrap_action.triggered.connect(self.edit_toggle_wrap) self.edit_menu.addAction(self.wrap_action) self.update_title() self.show()
class MSSMainWindow(QtWidgets.QMainWindow, ui.Ui_MSSMainWindow): """MSUI main window class. Provides user interface elements for managing flight tracks and views. """ viewsChanged = QtCore.pyqtSignal(name="viewsChanged") def __init__(self, *args): super(MSSMainWindow, self).__init__(*args) self.setupUi(self) self.setWindowIcon(QtGui.QIcon(icons('32x32'))) # This code is required in Windows 7 to use the icon set by setWindowIcon in taskbar # instead of the default Icon of python/pythonw try: import ctypes myappid = "mss.mss_pyui.{}".format(__version__) # arbitrary string ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) except (ImportError, AttributeError) as error: logging.debug("AttributeError, ImportError Exception %s", error) # Reference to the flight track that is currently displayed in the # views. self.active_flight_track = None self.last_save_directory = config_loader(dataset="data_dir", default=mss_default.data_dir) self.mscolab_window = None self.config_editor = None # Connect Qt SIGNALs: # =================== # File menu. self.actionNewFlightTrack.triggered.connect(functools.partial(self.create_new_flight_track, None, None)) self.actionOpenFlightTrack.triggered.connect(self.open_flight_track) self.actionActivateSelectedFlightTrack.triggered.connect(self.activate_selected_flight_track) self.actionCloseSelectedFlightTrack.triggered.connect(self.close_selected_flight_track) self.actionSaveActiveFlightTrack.triggered.connect(self.save_flight_track) self.actionSaveActiveFlightTrackAs.triggered.connect(self.save_flight_track_as) # Views menu. self.actionTopView.triggered.connect(self.create_new_view) self.actionSideView.triggered.connect(self.create_new_view) self.actionTableView.triggered.connect(self.create_new_view) # mscolab menu self.actionMscolabProjects.triggered.connect(self.activate_mscolab_window) # Help menu. self.actionOnlineHelp.triggered.connect(self.show_online_help) self.actionAboutMSUI.triggered.connect(self.show_about_dialog) # Config self.actionConfiguration.triggered.connect(self.open_config_file) # Flight Tracks. self.listFlightTracks.itemActivated.connect(self.activate_flight_track) # Views. self.listViews.itemActivated.connect(self.activate_sub_window) self.add_import_filter("CSV", "csv", load_from_csv, pickertag="filepicker_flightrack") self.add_export_filter("CSV", "csv", save_to_csv, pickertag="filepicker_flightrack") self._imported_plugins, self._exported_plugins = {}, {} self.add_plugins() preload_urls = config_loader(dataset="WMS_preload", default=[]) self.preload_wms(preload_urls) # Status Bar self.labelStatusbar.setText(self.status()) @staticmethod def preload_wms(urls): """ This method accesses a list of WMS servers and load their capability documents. :param urls: List of URLs """ pdlg = QtWidgets.QProgressDialog("Preloading WMS servers...", "Cancel", 0, len(urls)) pdlg.reset() pdlg.setValue(0) pdlg.setModal(True) pdlg.show() QtWidgets.QApplication.processEvents() for i, base_url in enumerate(urls): pdlg.setValue(i) QtWidgets.QApplication.processEvents() # initialize login cache from config file, but do not overwrite existing keys for key, value in config_loader(dataset="WMS_login", default={}).items(): if key not in constants.WMS_LOGIN_CACHE: constants.WMS_LOGIN_CACHE[key] = value username, password = constants.WMS_LOGIN_CACHE.get(base_url, (None, None)) try: request = requests.get(base_url) if pdlg.wasCanceled(): break wms = wms_control.MSSWebMapService(request.url, version='1.1.1', username=username, password=password) wms_control.WMS_SERVICE_CACHE[wms.url] = wms logging.info("Stored WMS info for '%s'", wms.url) except Exception as ex: logging.error("Error in preloading '%s': '%s'", type(ex), ex) if pdlg.wasCanceled(): break logging.debug("Contents of WMS_SERVICE_CACHE: %s", wms_control.WMS_SERVICE_CACHE.keys()) pdlg.close() def add_plugins(self): picker_default = config_loader( dataset="filepicker_default", default=mss_default.filepicker_default) self._imported_plugins = config_loader(dataset="import_plugins", default={}) for name in self._imported_plugins: extension, module, function = self._imported_plugins[name][:3] picker_type = picker_default if len(self._imported_plugins[name]) == 4: picker_type = self._imported_plugins[name][3] try: imported_module = importlib.import_module(module) # wildcard exception to be resilient against error introduced by user code except Exception as ex: logging.error("Error on import: %s: %s", type(ex), ex) QtWidgets.QMessageBox.critical( self, self.tr("file io plugin error import plugins"), self.tr("ERROR: Configuration\n\n{}\n\nthrows {} error:\n{}".format( self._imported_plugins, type(ex), ex))) continue try: self.add_import_filter(name, extension, getattr(imported_module, function), pickertype=picker_type) # wildcard exception to be resilient against error introduced by user code except Exception as ex: logging.error("Error on installing plugin: %s: %s", type(ex), ex) QtWidgets.QMessageBox.critical( self, self.tr("file io plugin error import plugins"), self.tr("ERROR: Configuration\n\n{}\n\nthrows {} error:\n{}".format( self._imported_plugins, type(ex), ex))) continue self._exported_plugins = config_loader(dataset="export_plugins", default={}) for name in self._exported_plugins: extension, module, function = self._exported_plugins[name][:3] picker_type = picker_default if len(self._exported_plugins[name]) == 4: picker_type = self._exported_plugins[name][3] try: imported_module = importlib.import_module(module) # wildcard exception to be resilient against error introduced by user code except Exception as ex: logging.error("Error on import: %s: %s", type(ex), ex) QtWidgets.QMessageBox.critical( self, self.tr("file io plugin error import plugins"), self.tr("ERROR: Configuration\n\n{}\n\nthrows {} error:\n{}".format( self._exported_plugins, type(ex), ex))) continue try: self.add_export_filter(name, extension, getattr(imported_module, function), pickertype=picker_type) # wildcard exception to be resilient against error introduced by user code except Exception as ex: logging.error("Error on installing plugin: %s: %s", type(ex), ex) QtWidgets.QMessageBox.critical( self, self.tr("file io plugin error"), self.tr("ERROR: Configuration for export {} plugins\n\n{}\n\nthrows error:\n{}".format( self._exported_plugins, type(ex), ex))) continue def remove_plugins(self): for name in self._imported_plugins: full_name = "actionImportFlightTrack" + clean_string(name) actions = [_x for _x in self.menuImport_Flight_Track.actions() if _x.objectName() == full_name] assert len(actions) == 1 self.menuImport_Flight_Track.removeAction(actions[0]) delattr(self, full_name) for name in self._exported_plugins: full_name = "actionExportFlightTrack" + clean_string(name) actions = [_x for _x in self.menuExport_Active_Flight_Track.actions() if _x.objectName() == full_name] assert len(actions) == 1 self.menuExport_Active_Flight_Track.removeAction(actions[0]) delattr(self, full_name) def add_import_filter(self, name, extension, function, pickertag=None, pickertype=None): full_name = "actionImportFlightTrack" + clean_string(name) if hasattr(self, full_name): raise ValueError("'{}' has already been set!".format(full_name)) action = QtWidgets.QAction(self) action.setObjectName(full_name) action.setText(QtCore.QCoreApplication.translate("MSSMainWindow", name, None)) self.menuImport_Flight_Track.addAction(action) def load_function_wrapper(self): filename = get_open_filename( self, "Import Flight Track", self.last_save_directory, "All Files (*." + extension + ")", pickertype=pickertype) if filename is not None: try: ft_name, new_waypoints = function(filename) # wildcard exception to be resilient against error introduced by user code except Exception as ex: logging.error("file io plugin error: %s %s", type(ex), ex) QtWidgets.QMessageBox.critical( self, self.tr("file io plugin error"), self.tr("ERROR: {} {}".format(type(ex), ex))) else: if not ft_name: ft_name = filename waypoints_model = ft.WaypointsTableModel(name=ft_name, waypoints=new_waypoints) listitem = QFlightTrackListWidgetItem(waypoints_model, self.listFlightTracks) listitem.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.listFlightTracks.setCurrentItem(listitem) self.activate_flight_track(listitem) setattr(self, full_name, types.MethodType(load_function_wrapper, self)) action.triggered.connect(getattr(self, full_name)) def add_export_filter(self, name, extension, function, pickertag=None, pickertype=None): full_name = "actionExportFlightTrack" + clean_string(name) if hasattr(self, full_name): raise ValueError("'{}' has already been set!".format(full_name)) action = QtWidgets.QAction(self) action.setObjectName(full_name) action.setText(QtCore.QCoreApplication.translate("MSSMainWindow", name, None)) self.menuExport_Active_Flight_Track.addAction(action) def save_function_wrapper(self): default_filename = os.path.join(self.last_save_directory, self.active_flight_track.name) + "." + extension filename = get_save_filename( self, "Export Flight Track", default_filename, name + " (*." + extension + ")", pickertype=pickertype) if filename is not None: try: function(filename, self.active_flight_track.name, self.active_flight_track.waypoints) # wildcard exception to be resilient against error introduced by user code except Exception as ex: logging.error("file io plugin error: %s %s", type(ex), ex) QtWidgets.QMessageBox.critical( self, self.tr("file io plugin error"), self.tr("ERROR: {} {}".format(type(ex), ex))) setattr(self, full_name, types.MethodType(save_function_wrapper, self)) action.triggered.connect(getattr(self, full_name)) def closeEvent(self, event): """Ask user if he/she wants to close the application. If yes, also close all views that are open. Overloads QtGui.QMainWindow.closeEvent(). This method is called if Qt receives a window close request for our application window. """ ret = QtWidgets.QMessageBox.warning( self, self.tr("Mission Support System"), self.tr("Do you want to close the Mission Support System application?"), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if ret == QtWidgets.QMessageBox.Yes: # Table View stick around after MainWindow closes - maybe some dangling reference? # This removes them for sure! self.listViews.clear() self.listFlightTracks.clear() # cleanup mscolab window if self.mscolab_window is not None: self.mscolab_window.close() event.accept() else: event.ignore() def create_new_view(self): """Method called when the user selects a new view to be opened. Creates a new instance of the view and adds a QActiveViewsListWidgetItem to the list of open views (self.listViews). """ layout = config_loader(dataset="layout", default=mss_default.layout) view_window = None if self.sender() == self.actionTopView: # Top view. view_window = topview.MSSTopViewWindow(model=self.active_flight_track) view_window.mpl.resize(layout['topview'][0], layout['topview'][1]) if layout["immutable"]: view_window.mpl.setFixedSize(layout['topview'][0], layout['topview'][1]) elif self.sender() == self.actionSideView: # Side view. view_window = sideview.MSSSideViewWindow(model=self.active_flight_track) view_window.mpl.resize(layout['sideview'][0], layout['sideview'][1]) if layout["immutable"]: view_window.mpl.setFixedSize(layout['sideview'][0], layout['sideview'][1]) elif self.sender() == self.actionTableView: # Table view. view_window = tableview.MSSTableViewWindow(model=self.active_flight_track) view_window.centralwidget.resize(layout['tableview'][0], layout['tableview'][1]) if view_window is not None: # Make sure view window will be deleted after being closed, not # just hidden (cf. Chapter 5 in PyQt4). view_window.setAttribute(QtCore.Qt.WA_DeleteOnClose) # Open as a non-modal window. view_window.show() # Add an entry referencing the new view to the list of views. listitem = QActiveViewsListWidgetItem(view_window, self.listViews, self.viewsChanged) view_window.viewCloses.connect(listitem.view_destroyed) self.listViews.setCurrentItem(listitem) self.viewsChanged.emit() def activate_sub_window(self, item): """When the user clicks on one of the open view or tool windows, this window is brought to the front. This function implements the slot to activate a window if the user selects it in the list of views or tools. """ # Restore the activated view and bring it to the front. item.window.showNormal() item.window.raise_() item.window.activateWindow() def close_mscolab_window(self): self.mscolab_window = None def activate_mscolab_window(self): # initiate mscolab window if self.mscolab_window is None: self.mscolab_window = mscolab.MSSMscolabWindow(parent=self) self.mscolab_window.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.mscolab_window.viewCloses.connect(self.close_mscolab_window) self.mscolab_window.show() else: self.mscolab_window.setWindowState(QtCore.Qt.WindowNoState) self.mscolab_window.raise_() self.mscolab_window.activateWindow() new_flight_track_counter = 0 def create_new_flight_track(self, template=None, filename=None): """Creates a new flight track model from a template. Adds a new entry to the list of flight tracks. Called when the user selects the 'new/open flight track' menu entries. Arguments: template -- copy the specified template to the new flight track (so that it is not empty). filename -- if not None, load the flight track in the specified file. """ if template is None: template = [] waypoints = config_loader(dataset="new_flighttrack_template", default=mss_default.new_flighttrack_template) default_flightlevel = config_loader(dataset="new_flighttrack_flightlevel", default=mss_default.new_flighttrack_flightlevel) for wp in waypoints: template.append(ft.Waypoint(flightlevel=default_flightlevel, location=wp)) if len(template) < 2: QtWidgets.QMessageBox.critical( self, self.tr("flighttrack template"), self.tr("ERROR:Flighttrack template in configuration is too short. " "Please add at least two valid locations.")) if filename is not None: waypoints_model = ft.WaypointsTableModel(filename=filename) else: # Create a new flight track from the waypoints template. self.new_flight_track_counter += 1 waypoints_model = ft.WaypointsTableModel( name="new flight track ({:d})".format(self.new_flight_track_counter)) # Make a copy of the template. Otherwise all new flight tracks would # use the same data structure in memory. template_copy = copy.deepcopy(template) waypoints_model.insertRows(0, rows=len(template_copy), waypoints=template_copy) # Create a new list entry for the flight track. Make the item name # editable. listitem = QFlightTrackListWidgetItem(waypoints_model, self.listFlightTracks) listitem.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.activate_flight_track(listitem) def open_config_file(self): """ Reads the config file Returns: """ ret = QtWidgets.QMessageBox.warning( self, self.tr("Mission Support System"), self.tr("Opening a config file will reset application. Continue?"), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if ret == QtWidgets.QMessageBox.Yes: self.config_editor = editor.EditorMainWindow(parent=self) def open_flight_track(self): """Slot for the 'Open Flight Track' menu entry. Opens a QFileDialog and passes the result to createNewFlightTrack(). """ filename = get_open_filename( self, "Open Flight Track", self.last_save_directory, "Flight Track Files (*.ftml)", pickertag="filepicker_flightrack") if filename is not None: try: if filename.endswith('.ftml'): self.create_new_flight_track(filename=filename) else: QtWidgets.QMessageBox.warning(self, "Open flight track", "No supported file extension recognized!\n{:}".format(filename)) except (SyntaxError, OSError, IOError) as ex: QtWidgets.QMessageBox.critical( self, self.tr("Problem while opening flight track FTML:"), self.tr("ERROR: {} {}".format(type(ex), ex))) def activate_selected_flight_track(self): item = self.listFlightTracks.currentItem() self.activate_flight_track(item) def close_selected_flight_track(self): """Slot to close the currently selected flight track. Flight tracks can only be closed if at least one other flight track remains open. The currently active flight track cannot be closed. """ if self.listFlightTracks.count() < 2: QtWidgets.QMessageBox.information(self, self.tr("Flight Track Management"), self.tr("At least one flight track has to be open.")) return item = self.listFlightTracks.currentItem() if item.flighttrack_model == self.active_flight_track: QtWidgets.QMessageBox.information(self, self.tr("Flight Track Management"), self.tr("Cannot close currently active flight track.")) return if item.flighttrack_model.modified: ret = QtWidgets.QMessageBox.warning(self, self.tr("Mission Support System"), self.tr("The flight track you are about to close has " "been modified. Close anyway?"), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if ret == QtWidgets.QMessageBox.Yes: self.listFlightTracks.takeItem(self.listFlightTracks.currentRow()) def save_flight_track(self): """Slot for the 'Save Active Flight Track As' menu entry. """ filename = self.active_flight_track.get_filename() if filename and filename.endswith('.ftml'): sel = QtWidgets.QMessageBox.question(self, "Save flight track", "Saving flight track to '{:s}'. Continue?".format(filename), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) if sel == QtWidgets.QMessageBox.Yes: try: self.active_flight_track.save_to_ftml(filename) except (OSError, IOError) as ex: QtWidgets.QMessageBox.critical( self, self.tr("Problem while saving flight track to FTML:"), self.tr("ERROR: {} {}".format(type(ex), ex))) else: self.save_flight_track_as() def save_flight_track_as(self): """Slot for the 'Save Active Flight Track As' menu entry. """ default_filename = os.path.join(self.last_save_directory, self.active_flight_track.name + ".ftml") filename = get_save_filename( self, "Save Flight Track", default_filename, "Flight Track (*.ftml)", pickertag="filepicker_flightrack") logging.debug("filename : '%s'", filename) if filename: self.last_save_directory = fs.path.dirname(filename) if filename.endswith('.ftml'): try: self.active_flight_track.save_to_ftml(filename) except (OSError, IOError) as ex: QtWidgets.QMessageBox.critical( self, self.tr("Problem while saving flight track to FTML:"), self.tr("ERROR: {} {}".format(type(ex), ex))) for idx in range(self.listFlightTracks.count()): if self.listFlightTracks.item(idx).flighttrack_model == self.active_flight_track: self.listFlightTracks.item(idx).setText(self.active_flight_track.name) else: QtWidgets.QMessageBox.warning(self, "Save flight track", "File extension is not '.ftml'!\n{:}".format(filename)) def activate_flight_track(self, item): """Set the currently selected flight track to be the active one, i.e. the one that is displayed in the views (only one flight track can be displayed at a time). """ self.active_flight_track = item.flighttrack_model for i in range(self.listViews.count()): view_item = self.listViews.item(i) view_item.window.setFlightTrackModel(self.active_flight_track) font = QtGui.QFont() for i in range(self.listFlightTracks.count()): self.listFlightTracks.item(i).setFont(font) font.setBold(True) item.setFont(font) def show_online_help(self): """Open Documentation in a browser""" QtGui.QDesktopServices.openUrl( QtCore.QUrl("http://mss.readthedocs.io/en/stable")) def show_about_dialog(self): """Show the 'About MSUI' dialog to the user. """ dlg = MSS_AboutDialog(parent=self) dlg.setModal(True) dlg.exec_() def status(self): if constants.CACHED_CONFIG_FILE is None: return ("Status : System Configuration") else: filename = constants.CACHED_CONFIG_FILE head_filename, tail_filename = os.path.split(filename) return("Status : User Configuration '" + tail_filename + "' loaded")
class MSColabAdminWindow(QtWidgets.QMainWindow, ui.Ui_MscolabAdminWindow): viewCloses = QtCore.pyqtSignal(name="viewCloses") def __init__(self, token, p_id, user, project_name, projects, conn, parent=None, mscolab_server_url=config_loader(dataset="default_MSCOLAB", default=mss_default.default_MSCOLAB)): """ token: access token p_id: project id conn: connection to send/receive socket messages """ super(MSColabAdminWindow, self).__init__(parent) self.setupUi(self) self.mscolab_server_url = mscolab_server_url self.token = token self.p_id = p_id self.user = user self.project_name = project_name self.projects = projects self.conn = conn self.addUsers = [] self.modifyUsers = [] # Button click handlers self.addUsersBtn.clicked.connect(self.add_selected_users) self.modifyUsersBtn.clicked.connect(self.modify_selected_users) self.deleteUsersBtn.clicked.connect(self.delete_selected_users) self.importPermissionsBtn.clicked.connect(self.import_permissions) self.selectAllAddBtn.clicked.connect(lambda: self.select_all(self.addUsersTable)) self.deselectAllAddBtn.clicked.connect(lambda: self.deselect_all(self.addUsersTable)) self.selectAllModifyBtn.clicked.connect(lambda: self.select_all(self.modifyUsersTable)) self.deselectAllModifyBtn.clicked.connect(lambda: self.deselect_all(self.modifyUsersTable)) # Search filter self.addUsersSearch.textChanged.connect(lambda text: self.search_user_filter(text, self.addUsersTable)) self.modifyUsersSearch.textChanged.connect(lambda text: self.search_user_filter(text, self.modifyUsersTable)) self.modifyUsersPermissionFilter.currentTextChanged.connect(self.apply_permission_filter) # Setting handlers for connection manager self.conn.signal_project_permissions_updated.connect(self.handle_permissions_updated) self.set_label_text() self.load_users_without_permission() self.load_users_with_permission() self.populate_import_permission_cb() def populate_table(self, table, users): table.setRowCount(0) for row_number, row_data in enumerate(users): table.insertRow(row_number) for col_number, item in enumerate(row_data): new_item = QtWidgets.QTableWidgetItem(item) table.setItem(row_number, col_number, new_item) def populate_import_permission_cb(self): self.importPermissionsCB.clear() for project in self.projects: if project['p_id'] != self.p_id: self.importPermissionsCB.addItem(project['path'], project['p_id']) def get_selected_userids(self, table, users): u_ids = [] selected_rows = table.selectionModel().selectedRows() for row in selected_rows: u_ids.append(users[row.row()][-1]) return u_ids def select_all(self, table): table.setFocus() for row_num in range(table.rowCount()): # Check if row is hidden due to some filter to exclude it if table.item(row_num, 0).isSelected() is False and table.isRowHidden(row_num) is False: table.selectRow(row_num) def deselect_all(self, table): table.setFocus() for row_num in range(table.rowCount()): # Check if row is hidden due to some filter to exclude it if table.item(row_num, 0).isSelected() and table.isRowHidden(row_num) is False: table.selectRow(row_num) # TODO: Think of a more cleaner implementation. def apply_filters(self, table, text_filter, permission_filter=None): for row_num in range(table.rowCount()): if text_filter in table.item(row_num, 0).text() or text_filter in table.item(row_num, 1).text(): if permission_filter: if permission_filter == "all" or permission_filter == table.item(row_num, 2).text(): table.showRow(row_num) else: table.hideRow(row_num) else: table.showRow(row_num) else: table.hideRow(row_num) def search_user_filter(self, text_filter, table): permission_filter = None if table == self.modifyUsersTable: permission_filter = str(self.modifyUsersPermissionFilter.currentText()) self.apply_filters(table, text_filter, permission_filter) def apply_permission_filter(self, permission_filter): self.modifyUsersTable.setFocus() text_filter = self.modifyUsersSearch.text() self.apply_filters(self.modifyUsersTable, text_filter, permission_filter) def set_label_text(self): self.projectNameLabel.setText(f"Project: {self.project_name}") self.usernameLabel.setText(f"Logged In: {self.user['username']}") def load_users_without_permission(self): self.addUsers = [] data = { "token": self.token, "p_id": self.p_id } url = url_join(self.mscolab_server_url, "users_without_permission") res = requests.get(url, data=data) res = res.json() self.addUsers = res["users"] self.populate_table(self.addUsersTable, self.addUsers) text_filter = self.addUsersSearch.text() self.apply_filters(self.addUsersTable, text_filter, None) def load_users_with_permission(self): self.modifyUsers = [] data = { "token": self.token, "p_id": self.p_id } url = url_join(self.mscolab_server_url, "users_with_permission") res = requests.get(url, data=data) res = res.json() self.modifyUsers = res["users"] self.populate_table(self.modifyUsersTable, self.modifyUsers) text_filter = self.modifyUsersSearch.text() permission_filter = str(self.modifyUsersPermissionFilter.currentText()) self.apply_filters(self.modifyUsersTable, text_filter, permission_filter) def add_selected_users(self): selected_userids = self.get_selected_userids(self.addUsersTable, self.addUsers) if len(selected_userids) == 0: return selected_access_level = str(self.addUsersPermission.currentText()) data = { "token": self.token, "p_id": self.p_id, "selected_userids": json.dumps(selected_userids), "selected_access_level": selected_access_level } url = url_join(self.mscolab_server_url, "add_bulk_permissions") res = requests.post(url, data=data) res = res.json() if res["success"]: # TODO: Do we need a success popup? self.load_users_without_permission() self.load_users_with_permission() else: show_popup(self, "Error", res["message"]) def modify_selected_users(self): selected_userids = self.get_selected_userids(self.modifyUsersTable, self.modifyUsers) if len(selected_userids) == 0: return selected_access_level = str(self.modifyUsersPermission.currentText()) data = { "token": self.token, "p_id": self.p_id, "selected_userids": json.dumps(selected_userids), "selected_access_level": selected_access_level } url = url_join(self.mscolab_server_url, "modify_bulk_permissions") res = requests.post(url, data=data) res = res.json() if res["success"]: self.load_users_without_permission() self.load_users_with_permission() else: self.show_error_popup(res["message"]) def delete_selected_users(self): selected_userids = self.get_selected_userids(self.modifyUsersTable, self.modifyUsers) if len(selected_userids) == 0: return data = { "token": self.token, "p_id": self.p_id, "selected_userids": json.dumps(selected_userids) } url = url_join(self.mscolab_server_url, "delete_bulk_permissions") res = requests.post(url, data=data) res = res.json() if res["success"]: self.load_users_without_permission() self.load_users_with_permission() else: self.show_error_popup(res["message"]) def import_permissions(self): import_p_id = self.importPermissionsCB.currentData(QtCore.Qt.UserRole) data = { "token": self.token, "current_p_id": self.p_id, "import_p_id": import_p_id } url = url_join(self.mscolab_server_url, 'import_permissions') res = requests.post(url, data=data).json() if res["success"]: self.load_users_without_permission() self.load_users_with_permission() else: show_popup(self, "Error", res["message"]) # Socket Events def handle_permissions_updated(self, u_id): if self.user["id"] == u_id: return show_popup(self, 'Alert', 'The permissions for this project were updated! The window is going to refresh.', 1) self.load_users_without_permission() self.load_users_with_permission() def closeEvent(self, event): self.viewCloses.emit()
class MSSMscolabWindow(QtWidgets.QMainWindow, ui.Ui_MSSMscolabWindow): """PyQt window implementing mscolab window """ name = "Mscolab" identifier = None viewCloses = QtCore.pyqtSignal(name="viewCloses") def __init__(self, parent=None, data_dir=mss_default.mss_dir, mscolab_server_url=mss_default.mscolab_server_url): """Set up user interface """ super(MSSMscolabWindow, self).__init__(parent) self.setupUi(self) self.loggedInWidget.hide() # if token is None, not authorized, else authorized self.token = None # User related signals self.connectMscolab.clicked.connect(self.connect_handler) self.addUser.clicked.connect(self.add_user_handler) self.loginButton.clicked.connect(self.authorize) self.logoutButton.clicked.connect(self.logout) self.deleteAccountButton.clicked.connect(self.delete_account) self.disconnectMscolab.clicked.connect(self.disconnect_handler) self.helpBtn.clicked.connect(self.open_help_dialog) # Project related signals self.addProject.clicked.connect(self.add_project_handler) self.importBtn.clicked.connect(self.handle_import) self.exportBtn.clicked.connect(self.handle_export) self.workLocallyCheckBox.stateChanged.connect(self.handle_work_locally_toggle) self.save_ft.clicked.connect(self.save_wp_mscolab) self.fetch_ft.clicked.connect(self.fetch_wp_mscolab) self.chatWindowBtn.clicked.connect(self.open_chat_window) self.adminWindowBtn.clicked.connect(self.open_admin_window) self.versionHistoryBtn.clicked.connect(self.open_version_history_window) self.deleteProjectBtn.clicked.connect(self.handle_delete_project) # View related signals self.topview.clicked.connect(self.open_topview) self.sideview.clicked.connect(self.open_sideview) self.tableview.clicked.connect(self.open_tableview) # int to store active pid self.active_pid = None # storing access_level to save network call self.access_level = None # storing project_name to save network call self.active_project_name = None # Storing project list to pass to admin window self.projects = None # store active_flight_path here as object self.waypoints_model = None # Store active project's file path self.local_ftml_file = None # store a reference of window in class self.open_windows_mscolab = [] # connection object to interact with sockets self.conn = None # store window instances self.active_windows = [] # assign ids to view-window self.id_count = 0 # project window self.chat_window = None # Admin Window self.admin_window = None # Version History Window self.version_window = None # Merge waypoints dialog self.merge_dialog = None # Mscolab help dialog self.help_dialog = None # set data dir, uri self.data_dir = data_dir self.mscolab_server_url = None self.disable_action_buttons() # disabling login, add user button. they are enabled when url is connected self.loginButton.setEnabled(False) self.addUser.setEnabled(False) self.disconnectMscolab.setEnabled(False) self.url.setEditable(True) self.url.setModel(MSCOLAB_URL_LIST) # fill value of mscolab url from config default_MSCOLAB = config_loader( dataset="default_MSCOLAB", default=mss_default.default_MSCOLAB) add_mscolab_urls(self.url, default_MSCOLAB) self.emailid.setText(config_loader(dataset="MSCOLAB_mailid", default="")) self.password.setText(config_loader(dataset="MSCOLAB_password", default="")) # fill value of mscolab url if found in QSettings storage self.settings = \ load_settings_qsettings('mscolab', default_settings={'recent_mscolab_urls': [], 'auth': {}, 'server_settings': {}}) if len(self.settings['recent_mscolab_urls']) > 0: add_mscolab_urls(self.url, self.settings['recent_mscolab_urls']) def disconnect_handler(self): self.logout() # enable and disable right buttons self.disconnectMscolab.setEnabled(False) self.loginButton.setEnabled(False) self.addUser.setEnabled(False) self.connectMscolab.setEnabled(True) # set mscolab_server_url to None self.mscolab_server_url = None def show_info(self, text): self.error_dialog = QtWidgets.QErrorMessage() self.error_dialog.showMessage(text) def connect_handler(self): try: url = str(self.url.currentText()) r = requests.get(url_join(url, 'status')) if r.text == "Mscolab server": # delete mscolab http_auth settings for the url if url not in self.settings["recent_mscolab_urls"]: self.settings["recent_mscolab_urls"].append(url) if self.mscolab_server_url in self.settings["auth"].keys(): del self.settings["auth"][self.mscolab_server_url] # assign new url to self.mscolab_server_url self.mscolab_server_url = url self.status.setText("Status: connected") # enable and disable right buttons self.loginButton.setEnabled(True) self.addUser.setEnabled(True) self.disconnectMscolab.setEnabled(True) self.connectMscolab.setEnabled(False) if self.mscolab_server_url not in self.settings["server_settings"].keys(): self.settings["server_settings"].update({self.mscolab_server_url: {}}) try: recent_email = self.settings["server_settings"][self.mscolab_server_url]["recent_email"] except KeyError: recent_email = "" self.emailid.setText(recent_email) save_settings_qsettings('mscolab', self.settings) else: show_popup(self, "Error", "Some unexpected error occurred. Please try again.") except requests.exceptions.ConnectionError: logging.debug("MSColab server isn't active") show_popup(self, "Error", "MSColab server isn't active") except requests.exceptions.InvalidSchema: logging.debug("invalid schema of url") show_popup(self, "Error", "Invalid Url Scheme!") except requests.exceptions.InvalidURL: logging.debug("invalid url") show_popup(self, "Error", "Invalid URL") except Exception as e: logging.debug("Error %s", str(e)) show_popup(self, "Error", "Some unexpected error occurred. Please try again.") def handle_import(self): file_path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select a file", "", "Flight track (*.ftml)") if file_path == "": return dir_path, file_name = fs.path.split(file_path) with open_fs(dir_path) as file_dir: xml_content = file_dir.readtext(file_name) try: model = ft.WaypointsTableModel(xml_content=xml_content) except SyntaxError: show_popup(self, "Import Failed", f"The file - {file_name}, does not contain valid XML") return self.waypoints_model = model if self.workLocallyCheckBox.isChecked(): self.waypoints_model.save_to_ftml(self.local_ftml_file) self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) else: self.conn.save_file(self.token, self.active_pid, xml_content, comment=None) self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) self.reload_view_windows() show_popup(self, "Import Success", f"The file - {file_name}, was imported successfully!", 1) def handle_export(self): file_path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save Flight track", self.active_project_name, "Flight track (*.ftml)") if file_path == "": return xml_doc = self.waypoints_model.get_xml_doc() dir_path, file_name = fs.path.split(file_path) with open_fs(dir_path).open(file_name, 'w') as file: xml_doc.writexml(file, indent=" ", addindent=" ", newl="\n", encoding="utf-8") def disable_project_buttons(self): self.save_ft.setEnabled(False) self.fetch_ft.setEnabled(False) self.topview.setEnabled(False) self.sideview.setEnabled(False) self.tableview.setEnabled(False) self.workLocallyCheckBox.setEnabled(False) self.importBtn.setEnabled(False) self.exportBtn.setEnabled(False) self.chatWindowBtn.setEnabled(False) self.adminWindowBtn.setEnabled(False) self.versionHistoryBtn.setEnabled(False) self.deleteProjectBtn.setEnabled(False) self.helperTextLabel.setVisible(False) def disable_action_buttons(self): # disable some buttons to be activated after successful login or project activate self.addProject.setEnabled(False) self.disable_project_buttons() def authenticate(self, data, r, url): counter = 0 while r.status_code == 401 and counter < 5: dlg = MSCOLAB_AuthenticationDialog(parent=self) dlg.setModal(True) if dlg.exec_() == QtWidgets.QDialog.Accepted: username, password = dlg.getAuthInfo() self.settings["auth"][self.mscolab_server_url] = (username, password) # save to cache save_settings_qsettings('mscolab', self.settings) s = requests.Session() s.auth = (username, password) s.headers.update({'x-test': 'true'}) r = s.post(url, data=data) counter += 1 return r def add_project_handler(self): if self.token is None: self.error_dialog = QtWidgets.QErrorMessage() self.error_dialog.showMessage('Please login to use this feature') return else: logging.debug(self.token) self.proj_diag = QtWidgets.QDialog() self.add_proj_dialog = add_project_ui.Ui_addProjectDialog() self.add_proj_dialog.setupUi(self.proj_diag) self.add_proj_dialog.f_content = None self.add_proj_dialog.buttonBox.accepted.connect(self.add_project) self.add_proj_dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(False) self.add_proj_dialog.path.textChanged.connect(self.check_and_enable_project_accept) self.add_proj_dialog.description.textChanged.connect(self.check_and_enable_project_accept) self.add_proj_dialog.browse.clicked.connect(self.set_exported_file) self.proj_diag.show() def check_and_enable_project_accept(self): if self.add_proj_dialog.path.text() != "" and self.add_proj_dialog.description.toPlainText() != "": self.add_proj_dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(True) else: self.add_proj_dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(False) def set_exported_file(self): file_path = get_open_filename( self, "Open ftml file", "", "Flight Track Files (*.ftml)") if file_path is not None: file_name = fs.path.basename(file_path) with open_fs(fs.path.dirname(file_path)) as file_dir: file_content = file_dir.readtext(file_name) self.add_proj_dialog.f_content = file_content self.add_proj_dialog.selectedFile.setText(file_name) def add_project(self): path = self.add_proj_dialog.path.text() description = self.add_proj_dialog.description.toPlainText() if not path: self.error_dialog = QtWidgets.QErrorMessage() self.error_dialog.showMessage('Path can\'t be empty') return elif not description: self.error_dialog = QtWidgets.QErrorMessage() self.error_dialog.showMessage('Description can\'t be empty') return data = { "token": self.token, "path": path, "description": description } if self.add_proj_dialog.f_content is not None: data["content"] = self.add_proj_dialog.f_content r = requests.post('{}/create_project'.format(self.mscolab_server_url), data=data) if r.text == "True": self.error_dialog = QtWidgets.QErrorMessage() self.error_dialog.showMessage('Your project was created successfully') self.add_projects() p_id = self.get_recent_pid() self.conn.handle_new_room(p_id) else: self.error_dialog = QtWidgets.QErrorMessage() self.error_dialog.showMessage('The path already exists') def add_user_handler(self): self.user_diag = QtWidgets.QDialog() self.add_user_dialog = add_user_ui.Ui_addUserDialog() self.add_user_dialog.setupUi(self.user_diag) self.add_user_dialog.buttonBox.accepted.connect(self.add_user) self.user_diag.show() def add_user(self): for key, value in config_loader(dataset="MSC_login", default={}).items(): if key not in constants.MSC_LOGIN_CACHE: constants.MSC_LOGIN_CACHE[key] = value auth = constants.MSC_LOGIN_CACHE.get(self.mscolab_server_url, (None, None)) emailid = self.add_user_dialog.emailid.text() password = self.add_user_dialog.password.text() re_password = self.add_user_dialog.rePassword.text() username = self.add_user_dialog.username.text() if password == re_password: data = { "email": emailid, "password": password, "username": username } s = requests.Session() s.auth = (auth[0], auth[1]) s.headers.update({'x-test': 'true'}) url = '{}/register'.format(self.mscolab_server_url) r = s.post(url, data=data) if r.status_code == 401: r = self.authenticate(data, r, url) if r.status_code == 201: constants.MSC_LOGIN_CACHE[self.mscolab_server_url] = (username, password) if r.status_code == 201: self.error_dialog = QtWidgets.QErrorMessage() self.error_dialog.showMessage('You are registered, you can now log in.') else: self.error_dialog = QtWidgets.QErrorMessage() self.error_dialog.showMessage(r.json()["message"]) else: self.error_dialog = QtWidgets.QErrorMessage() self.error_dialog.showMessage('Oh no, your passwords don\'t match') def close_help_dialog(self): self.help_dialog = None def open_help_dialog(self): if self.help_dialog is not None: self.help_dialog.raise_() self.help_dialog.activateWindow() else: self.help_dialog = MscolabHelpDialog(self) self.help_dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.help_dialog.viewCloses.connect(self.close_help_dialog) self.help_dialog.show() def handle_delete_project(self): entered_project_name, ok = QtWidgets.QInputDialog.getText( self, self.tr('Delete Project'), self.tr(f"You're about to delete the project - '{self.active_project_name}'. " f"Enter the project name to confirm: ")) if ok: if entered_project_name == self.active_project_name: data = { "token": self.token, "p_id": self.active_pid } url = url_join(self.mscolab_server_url, 'delete_project') try: res = requests.post(url, data=data) res.raise_for_status() except requests.exceptions.RequestException as e: logging.debug(e) show_popup(self, "Error", "Some error occurred! Could not delete project.") else: show_popup(self, "Error", "Entered project name did not match!") def open_chat_window(self): if self.active_pid is None: return if self.chat_window is not None: self.chat_window.raise_() self.chat_window.activateWindow() return self.chat_window = mp.MSColabProjectWindow(self.token, self.active_pid, self.user, self.active_project_name, self.access_level, self.conn, mscolab_server_url=self.mscolab_server_url) self.chat_window.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.chat_window.viewCloses.connect(self.close_chat_window) self.chat_window.reloadWindows.connect(self.reload_windows_slot) self.chat_window.show() def close_chat_window(self): self.chat_window = None def open_admin_window(self): if self.active_pid is None: return if self.admin_window is not None: self.admin_window.raise_() self.admin_window.activateWindow() return self.admin_window = maw.MSColabAdminWindow(self.token, self.active_pid, self.user, self.active_project_name, self.projects, self.conn, mscolab_server_url=self.mscolab_server_url) self.admin_window.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.admin_window.viewCloses.connect(self.close_admin_window) self.admin_window.show() def close_admin_window(self): self.admin_window = None def open_version_history_window(self): if self.active_pid is None: return if self.version_window is not None: self.version_window.raise_() self.version_window.activateWindow() return self.version_window = mvh.MSColabVersionHistory(self.token, self.active_pid, self.user, self.active_project_name, self.conn, mscolab_server_url=self.mscolab_server_url) self.version_window.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.version_window.viewCloses.connect(self.close_version_history_window) self.version_window.reloadWindows.connect(self.reload_windows_slot) self.version_window.show() def close_version_history_window(self): self.version_window = None def create_local_project_file(self): with open_fs(self.data_dir) as mss_dir: rel_file_path = fs.path.join('local_mscolab_data', self.user['username'], self.active_project_name, 'mscolab_project.ftml') if mss_dir.exists(rel_file_path) is True: return mss_dir.makedirs(fs.path.dirname(rel_file_path)) server_data = self.waypoints_model.get_xml_content() mss_dir.writetext(rel_file_path, server_data) def handle_work_locally_toggle(self): if self.workLocallyCheckBox.isChecked(): if self.version_window is not None: self.version_window.close() self.create_local_project_file() self.local_ftml_file = fs.path.join(self.data_dir, 'local_mscolab_data', self.user['username'], self.active_project_name, 'mscolab_project.ftml') self.helperTextLabel.setText( self.tr("Working On: Local File. Your changes are only available to you." "To save your changes with everyone, use the \"Save to Server\" button.")) self.save_ft.setEnabled(True) self.fetch_ft.setEnabled(True) self.versionHistoryBtn.setEnabled(False) self.reload_local_wp() else: self.local_ftml_file = None self.helperTextLabel.setText( self.tr("Working On: Shared File. All your changes will be shared with everyone." "Turn on work locally to work on local flight track file")) self.save_ft.setEnabled(False) self.fetch_ft.setEnabled(False) if self.access_level == "admin" or self.access_level == "creator": self.versionHistoryBtn.setEnabled(True) self.waypoints_model = None self.load_wps_from_server() self.reload_view_windows() def authorize(self): for key, value in config_loader(dataset="MSC_login", default={}).items(): if key not in constants.MSC_LOGIN_CACHE: constants.MSC_LOGIN_CACHE[key] = value auth = constants.MSC_LOGIN_CACHE.get(self.mscolab_server_url, (None, None)) # get mscolab /token http auth credentials from cache emailid = self.emailid.text() password = self.password.text() data = { "email": emailid, "password": password } s = requests.Session() s.auth = (auth[0], auth[1]) s.headers.update({'x-test': 'true'}) url = self.mscolab_server_url + '/token' r = s.post(url, data=data) if r.status_code == 401: r = self.authenticate(data, r, url) if r.status_code == 200: constants.MSC_LOGIN_CACHE[self.mscolab_server_url] = (auth[0], auth[1]) self.after_authorize(emailid, r) elif r.text == "False": # popup that has wrong credentials self.error_dialog = QtWidgets.QErrorMessage() self.error_dialog.showMessage('Oh no, your credentials were incorrect.') else: # remove the login modal and put text there self.after_authorize(emailid, r) def after_authorize(self, emailid, r): _json = json.loads(r.text) self.token = _json["token"] self.user = _json["user"] self.label.setText(self.tr(f"Welcome, {self.user['username']}")) self.password.setText("") self.loggedInWidget.show() self.loginWidget.hide() self.add_projects() # create socket connection here self.conn = sc.ConnectionManager(self.token, user=self.user, mscolab_server_url=self.mscolab_server_url) self.conn.signal_reload.connect(self.reload_window) self.conn.signal_new_permission.connect(self.render_new_permission) self.conn.signal_update_permission.connect(self.handle_update_permission) self.conn.signal_revoke_permission.connect(self.handle_revoke_permission) self.conn.signal_project_deleted.connect(self.handle_project_deleted) # activate add project button here self.addProject.setEnabled(True) self.settings['server_settings'][self.mscolab_server_url].update({"recent_email": emailid}) save_settings_qsettings('mscolab', self.settings) def add_projects(self): # add projects data = { "token": self.token } r = requests.get(self.mscolab_server_url + '/projects', data=data) _json = json.loads(r.text) self.projects = _json["projects"] self.add_projects_to_ui(self.projects) def get_recent_pid(self): """ get most recent project's p_id # ToDo can be merged with get_recent_project """ data = { "token": self.token } r = requests.get(self.mscolab_server_url + '/projects', data=data) _json = json.loads(r.text) projects = _json["projects"] return projects[-1]["p_id"] def get_recent_project(self): """ get most recent project """ data = { "token": self.token } r = requests.get(self.mscolab_server_url + '/projects', data=data) _json = json.loads(r.text) projects = _json["projects"] return projects[-1] def add_projects_to_ui(self, projects): logging.debug("adding projects to ui") self.listProjects.clear() selectedProject = None for project in projects: project_desc = f'{project["path"]} - {project["access_level"]}' widgetItem = QtWidgets.QListWidgetItem(project_desc, parent=self.listProjects) widgetItem.p_id = project["p_id"] widgetItem.access_level = project["access_level"] if widgetItem.p_id == self.active_pid: selectedProject = widgetItem self.listProjects.addItem(widgetItem) if selectedProject is not None: self.listProjects.setCurrentItem(selectedProject) self.listProjects.itemActivated.emit(selectedProject) self.listProjects.itemActivated.connect(self.set_active_pid) def force_close_view_windows(self): for window in self.active_windows: window.handle_force_close() self.active_windows = [] def set_active_pid(self, item): if item.p_id == self.active_pid: return # close all hanging window self.force_close_view_windows() self.close_external_windows() # Turn off work locally toggle self.workLocallyCheckBox.blockSignals(True) self.workLocallyCheckBox.setChecked(False) self.workLocallyCheckBox.blockSignals(False) self.save_ft.setEnabled(False) self.fetch_ft.setEnabled(False) # set active_pid here self.active_pid = item.p_id self.access_level = item.access_level self.active_project_name = item.text().split("-")[0].strip() self.waypoints_model = None # set active flightpath here self.load_wps_from_server() # enable project specific buttons self.helperTextLabel.setVisible(True) self.helperTextLabel.setText(self.tr("Working On: Shared File. All your changes will be shared with everyone." "Turn on work locally to work on local flight track file")) self.importBtn.setEnabled(True) self.exportBtn.setEnabled(True) self.topview.setEnabled(True) self.sideview.setEnabled(True) self.tableview.setEnabled(True) self.workLocallyCheckBox.setEnabled(True) if self.access_level == "viewer" or self.access_level == "collaborator": if self.access_level == "viewer": self.chatWindowBtn.setEnabled(False) else: self.chatWindowBtn.setEnabled(True) self.adminWindowBtn.setEnabled(False) self.versionHistoryBtn.setEnabled(False) else: self.adminWindowBtn.setEnabled(True) self.chatWindowBtn.setEnabled(True) self.versionHistoryBtn.setEnabled(True) if self.access_level == "creator": self.deleteProjectBtn.setEnabled(True) else: self.deleteProjectBtn.setEnabled(False) # change font style for selected font = QtGui.QFont() for i in range(self.listProjects.count()): self.listProjects.item(i).setFont(font) font.setBold(True) item.setFont(font) def reload_wps_from_server(self): if self.active_pid is None: return self.load_wps_from_server() self.reload_view_windows() def request_wps_from_server(self): data = { "token": self.token, "p_id": self.active_pid } r = requests.get(self.mscolab_server_url + '/get_project', data=data) xml_content = json.loads(r.text)["content"] return xml_content def load_wps_from_server(self): if self.workLocallyCheckBox.isChecked(): return xml_content = self.request_wps_from_server() self.waypoints_model = ft.WaypointsTableModel(xml_content=xml_content) self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) def open_topview(self): # showing dummy info dialog if self.active_pid is None: return self.create_view_window("topview") def open_sideview(self): # showing dummy info dialog if self.active_pid is None: return self.create_view_window("sideview") def open_tableview(self): # showing dummy info dialog if self.active_pid is None: return self.create_view_window("tableview") def create_view_window(self, _type): for active_window in self.active_windows: if active_window.view_type == _type: active_window.raise_() active_window.activateWindow() return if _type == "topview": view_window = topview.MSSTopViewWindow(model=self.waypoints_model, parent=self.listProjects, _id=self.id_count) view_window.view_type = "topview" elif _type == "sideview": view_window = sideview.MSSSideViewWindow(model=self.waypoints_model, parent=self.listProjects, _id=self.id_count) view_window.view_type = "sideview" else: view_window = tableview.MSSTableViewWindow(model=self.waypoints_model, parent=self.listProjects, _id=self.id_count) view_window.view_type = "tableview" if self.access_level == "viewer": self.disable_navbar_action_buttons(_type, view_window) view_window.setAttribute(QtCore.Qt.WA_DeleteOnClose) view_window.show() view_window.viewClosesId.connect(self.handle_view_close) self.active_windows.append(view_window) # increment id_count self.id_count += 1 def disable_navbar_action_buttons(self, _type, view_window): """ _type: view type (topview, sideview, tableview) view_window: PyQt view window function disables some control, used if access_level is not appropriate """ if _type == "topview" or _type == "sideview": actions = view_window.mpl.navbar.actions() for action in actions: action_text = action.text() if action_text == "Ins WP" or action_text == "Del WP" or action_text == "Mv WP": action.setEnabled(False) else: # _type == tableview view_window.btAddWayPointToFlightTrack.setEnabled(False) view_window.btCloneWaypoint.setEnabled(False) view_window.btDeleteWayPoint.setEnabled(False) view_window.btInvertDirection.setEnabled(False) def enable_navbar_action_buttons(self, _type, view_window): """ _type: view type (topview, sideview, tableview) view_window: PyQt view window function enables some control, used if access_level is appropriate """ if _type == "topview" or _type == "sideview": actions = view_window.mpl.navbar.actions() for action in actions: action_text = action.text() if action_text == "Ins WP" or action_text == "Del WP" or action_text == "Mv WP": action.setEnabled(True) else: # _type == tableview view_window.btAddWayPointToFlightTrack.setEnabled(True) view_window.btCloneWaypoint.setEnabled(True) view_window.btDeleteWayPoint.setEnabled(True) view_window.btInvertDirection.setEnabled(True) def logout(self): self.clean_up_window() def delete_account(self): w = QtWidgets.QWidget() qm = QtWidgets.QMessageBox reply = qm.question(w, self.tr('Continue?'), self.tr("You're about to delete your account. You cannot undo this operation!"), qm.Yes, qm.No) if reply == QtWidgets.QMessageBox.No: return data = { "token": self.token } requests.post(self.mscolab_server_url + '/delete_user', data=data) self.clean_up_window() def close_external_windows(self): if self.chat_window is not None: self.chat_window.close() if self.admin_window is not None: self.admin_window.close() if self.version_window is not None: self.version_window.close() def clean_up_window(self): # delete token and show login widget-items self.token = None # delete active-project-id self.active_pid = None # delete active access_level self.access_level = None # delete active project_name self.active_project_name = None # delete local file name self.local_ftml_file = None # clear projects list here self.loggedInWidget.hide() self.loginWidget.show() # clear project listing self.listProjects.clear() # disconnect socket if self.conn is not None: self.conn.disconnect() self.conn = None # close all hanging window self.force_close_view_windows() self.close_external_windows() self.disable_action_buttons() # delete mscolab http_auth settings for the url if self.mscolab_server_url in self.settings["auth"].keys(): del self.settings["auth"][self.mscolab_server_url] save_settings_qsettings('mscolab', self.settings) def save_wp_mscolab(self, comment=None): server_xml = self.request_wps_from_server() server_waypoints_model = ft.WaypointsTableModel(xml_content=server_xml) self.merge_dialog = MscolabMergeWaypointsDialog(self.waypoints_model, server_waypoints_model, parent=self) if self.merge_dialog.exec_(): xml_content = self.merge_dialog.get_values() if xml_content is not None: self.conn.save_file(self.token, self.active_pid, xml_content, comment=comment) self.waypoints_model = ft.WaypointsTableModel(xml_content=xml_content) self.waypoints_model.save_to_ftml(self.local_ftml_file) self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) self.reload_view_windows() show_popup(self, "Success", "New Waypoints Saved To Server!", icon=1) self.merge_dialog = None def handle_waypoints_changed(self): if self.workLocallyCheckBox.isChecked(): self.waypoints_model.save_to_ftml(self.local_ftml_file) else: xml_content = self.waypoints_model.get_xml_content() self.conn.save_file(self.token, self.active_pid, xml_content, comment=None) def reload_view_windows(self): for window in self.active_windows: window.setFlightTrackModel(self.waypoints_model) if hasattr(window, 'mpl'): window.mpl.canvas.waypoints_interactor.redraw_figure() def reload_local_wp(self): self.waypoints_model = ft.WaypointsTableModel(filename=self.local_ftml_file, data_dir=self.data_dir) self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) self.reload_view_windows() def fetch_wp_mscolab(self): server_xml = self.request_wps_from_server() server_waypoints_model = ft.WaypointsTableModel(xml_content=server_xml) self.merge_dialog = MscolabMergeWaypointsDialog(self.waypoints_model, server_waypoints_model, True, self) if self.merge_dialog.exec_(): xml_content = self.merge_dialog.get_values() if xml_content is not None: self.waypoints_model = ft.WaypointsTableModel(xml_content=xml_content) self.waypoints_model.save_to_ftml(self.local_ftml_file) self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) self.reload_view_windows() show_popup(self, "Success", "New Waypoints Fetched To Local File!", icon=1) self.merge_dialog = None @QtCore.Slot(int, int, str) def handle_update_permission(self, p_id, u_id, access_level): """ p_id: project id u_id: user id access_level: updated access level function updates existing permissions and related control availability """ if u_id == self.user["id"]: # update table of projects project_name = None for i in range(self.listProjects.count()): item = self.listProjects.item(i) if item.p_id == p_id: desc = item.text().split(' - ') project_name = desc[0] desc[-1] = access_level desc = ' - '.join(desc) item.setText(desc) item.access_level = access_level break if project_name is not None: show_popup(self, "Permission Updated", f"Your access level to project - {project_name} was updated to {access_level}!", 1) if p_id != self.active_pid: return self.access_level = access_level # Close mscolab windows based on new access_level and update their buttons if self.access_level == "collaborator" or self.access_level == "viewer": self.adminWindowBtn.setEnabled(False) self.versionHistoryBtn.setEnabled(False) if self.admin_window is not None: self.admin_window.close() if self.version_window is not None: self.version_window.close() else: self.adminWindowBtn.setEnabled(True) self.versionHistoryBtn.setEnabled(True) if self.access_level == "viewer": self.chatWindowBtn.setEnabled(False) if self.chat_window is not None: self.chat_window.close() else: self.chatWindowBtn.setEnabled(True) # update view window nav elements if open for window in self.active_windows: _type = window.view_type if self.access_level == "viewer": self.disable_navbar_action_buttons(_type, window) else: self.enable_navbar_action_buttons(_type, window) # update chat window if open if self.chat_window is not None: self.chat_window.load_users() def delete_project_from_list(self, p_id): logging.debug('delete project p_id: %s and active_id is: %s' % (p_id, self.active_pid)) if self.active_pid == p_id: logging.debug('delete_project_from_list doing: %s' % p_id) self.active_pid = None self.access_level = None self.active_project_name = None self.helperTextLabel.setVisible(False) self.force_close_view_windows() self.close_external_windows() self.disable_project_buttons() # Update project list remove_item = None for i in range(self.listProjects.count()): item = self.listProjects.item(i) if item.p_id == p_id: remove_item = item if remove_item is not None: logging.debug("remove_item: %s" % remove_item) self.listProjects.takeItem(self.listProjects.row(remove_item)) return remove_item.text().split(' - ')[0] @QtCore.Slot(int, int) def handle_revoke_permission(self, p_id, u_id): if u_id == self.user["id"]: project_name = self.delete_project_from_list(p_id) show_popup(self, "Permission Revoked", f'Your access to project - "{project_name}" was revoked!', icon=1) @QtCore.Slot() def reload_windows_slot(self): self.reload_window(self.active_pid) @QtCore.Slot(int, int) def render_new_permission(self, p_id, u_id): """ p_id: project id u_id: user id to render new permission if added """ data = { 'token': self.token } r = requests.get(self.mscolab_server_url + '/user', data=data) _json = json.loads(r.text) if _json['user']['id'] == u_id: project = self.get_recent_project() project_desc = f'{project["path"]} - {project["access_level"]}' widgetItem = QtWidgets.QListWidgetItem(project_desc, parent=self.listProjects) widgetItem.p_id = project["p_id"] widgetItem.access_level = project["access_level"] self.listProjects.addItem(widgetItem) if self.chat_window is not None: self.chat_window.load_users() @QtCore.Slot(int) def handle_project_deleted(self, p_id): project_name = self.delete_project_from_list(p_id) show_popup(self, "Success", f'Project "{project_name}" was deleted!', icon=1) @QtCore.Slot(int) def reload_window(self, value): if self.active_pid != value or self.workLocallyCheckBox.isChecked(): return self.reload_wps_from_server() @QtCore.Slot(int) def handle_view_close(self, value): logging.debug("removing stale window") for index, window in enumerate(self.active_windows): if window._id == value: del self.active_windows[index] def setIdentifier(self, identifier): self.identifier = identifier def closeEvent(self, event): if self.help_dialog is not None: self.help_dialog.close() self.clean_up_window() self.viewCloses.emit()
class MplTopViewCanvas(MplCanvas): """Specialised MplCanvas that draws a top view (map), together with a flight track, trajectories and other items. """ redrawn = QtCore.pyqtSignal(name="redrawn") def __init__(self, settings=None): """ """ super(MplTopViewCanvas, self).__init__() self.waypoints_interactor = None self.satoverpasspatch = [] self.kmloverlay = None self.map = None self.basename = "topview" # Axes and image object to display the legend graphic, if available. self.legax = None self.legimg = None # Set map appearance from parameter or, if not specified, to default # values. self.set_map_appearance(settings) # Progress dialog to inform the user about map redraws. self.pdlg = QtWidgets.QProgressDialog("redrawing map...", "Cancel", 0, 10, self) self.pdlg.close() def init_map(self, model=None, **kwargs): """Set up the map view. """ ax = self.ax self.map = mpl_map.MapCanvas(appearance=self.get_map_appearance(), resolution="l", area_thresh=1000., ax=ax, **kwargs) ax.set_autoscale_on(False) ax.set_title("Top view", horizontalalignment="left", x=0) self.draw() # necessary? if model: self.set_waypoints_model(model) def set_waypoints_model(self, model): """Set the WaypointsTableModel defining the flight track. If no model had been set before, create a new interactor object on the model to let the user interactively move the altitude of the waypoints. """ self.waypoints_model = model if self.waypoints_interactor: self.waypoints_interactor.set_waypoints_model(model) else: # Create a path interactor object. The interactor object connects # itself to the change() signals of the flight track data model. appearance = self.get_map_appearance() self.waypoints_interactor = mpl_pi.HPathInteractor( self.map, self.waypoints_model, linecolor=appearance["colour_ft_vertices"], markerfacecolor=appearance["colour_ft_waypoints"]) self.waypoints_interactor.set_vertices_visible( appearance["draw_flighttrack"]) def redraw_map(self, kwargs_update=None): """Redraw map canvas. Executed on clicked() of btMapRedraw. See MapCanvas.update_with_coordinate_change(). After the map redraw, coordinates of all objects overlain on the map have to be updated. """ # remove legend self.draw_legend(None) # Show the progress dialog, since the retrieval can take a few seconds. self.pdlg.setValue(0) self.pdlg.show() QtWidgets.QApplication.processEvents() logging.debug("redrawing map") # 1) STORE COORDINATES OF NON-MAP OBJECTS IN LAT/LON. # (Currently none.) self.pdlg.setValue(1) QtWidgets.QApplication.processEvents() # 2) UPDATE MAP. self.map.update_with_coordinate_change(kwargs_update) self.draw() # this one is required to trigger a # drawevent to update the background # in waypoints_interactor() self.pdlg.setValue(5) QtWidgets.QApplication.processEvents() # 3) UPDATE COORDINATES OF NON-MAP OBJECTS. self.pdlg.setValue(8) QtWidgets.QApplication.processEvents() for segment in self.satoverpasspatch: segment.update() if self.kmloverlay: self.kmloverlay.update() self.draw_metadata("Top view") # Update in case of a projection change self.waypoints_interactor.update() self.pdlg.setValue(10) QtWidgets.QApplication.processEvents() logging.debug("finished redrawing map") self.pdlg.close() # Emit signal so other parts of the module can react to a redraw event. self.redrawn.emit() def get_crs(self): """Get the coordinate reference system of the displayed map. """ return self.map.crs def getBBOX(self): """ Get the bounding box of the map (returns a 4-tuple llx, lly, urx, ury) in degree or meters. """ axis = self.ax.axis() if self.map.bbox_units == "degree": # Convert the current axis corners to lat/lon coordinates. axis0, axis2 = self.map(axis[0], axis[2], inverse=True) axis1, axis3 = self.map(axis[1], axis[3], inverse=True) bbox = (axis0, axis2, axis1, axis3) elif self.map.bbox_units.startswith("meter"): center_x, center_y = self.map( *(float(_x) for _x in self.map.bbox_units[6:-1].split(","))) bbox = (axis[0] - center_x, axis[2] - center_y, axis[1] - center_x, axis[3] - center_y) else: bbox = axis[0], axis[2], axis[1], axis[3] return bbox def clear_figure(self): logging.debug("Removing image") if self.map.image is not None: self.map.image.remove() self.map.image = None self.ax.set_title("Top view", horizontalalignment="left", x=0) self.ax.figure.canvas.draw() def draw_image(self, img): """Draw the image img on the current plot. """ logging.debug("plotting image..") self.wms_image = self.map.imshow(img, interpolation="nearest", origin=PIL_IMAGE_ORIGIN) # NOTE: imshow always draws the images to the lowest z-level of the # plot. # See these mailing list entries: # http://www.mail-archive.com/[email protected]/msg05955.html # http://old.nabble.com/Re%3A--Matplotlib-users--imshow-zorder-tt19047314.html#a19047314 # # Question: Is this an issue for us or do we always want the images in the back # anyhow? At least we need to remove filled continents here. # self.map.set_fillcontinents_visible(False) # ** UPDATE 2011/01/14 ** seems to work with version 1.0! logging.debug("done.") def draw_legend(self, img): """Draw the legend graphics img on the current plot. Adds new axes to the plot that accomodate the legend. """ # If the method is called with a "None" image, the current legend # graphic should be removed (if one exists). if self.legimg is not None: logging.debug("removing image %s", self.legimg) self.legimg.remove() self.legimg = None if img is not None: # The size of the legend axes needs to be given in relative figure # coordinates. To determine those from the legend graphics size in # pixels, we need to determine the size of the currently displayed # figure in pixels. figsize_px = self.fig.get_size_inches() * self.fig.get_dpi() ax_extent_x = float(img.size[0]) / figsize_px[0] ax_extent_y = float(img.size[1]) / figsize_px[1] # If no legend axes have been created, do so now. if self.legax is None: # Main axes instance of mplwidget has zorder 99. self.legax = self.fig.add_axes( [1 - ax_extent_x, 0.01, ax_extent_x, ax_extent_y], frameon=False, xticks=[], yticks=[], label="ax2", zorder=0) self.legax.patch.set_facecolor("None") # If axes exist, adjust their position. else: self.legax.set_position( [1 - ax_extent_x, 0.01, ax_extent_x, ax_extent_y]) # Plot the new legimg in the legax axes. self.legimg = self.legax.imshow(img, origin=PIL_IMAGE_ORIGIN, aspect="equal", interpolation="nearest") self.draw() # required so that it is actually drawn... QtWidgets.QApplication.processEvents() def plot_satellite_overpass(self, segments): """Plots a satellite track on top of the map. """ # If track is currently plotted on the map, remove it. for segment in self.satoverpasspatch: segment.remove() self.satoverpasspatch = [] if segments: # Create a new patch. self.satoverpasspatch = [ mpl_map.SatelliteOverpassPatch(self.map, segment) for segment in segments ] self.draw() def plot_kml(self, kmloverlay): """Plots a satellite track on top of the map. """ if self.kmloverlay: # If track is currently plotted on the map, remove it. self.kmloverlay.remove() if not kmloverlay: self.kmloverlay = None self.draw() if kmloverlay: # Create a new patch. self.kmloverlay = kmloverlay def set_map_appearance(self, settings_dict): """Apply settings from dictionary 'settings_dict' to the view. If settings is None, apply default settings. """ # logging.debug("applying map appearance settings %s." % settings) settings = { "draw_graticule": True, "draw_coastlines": True, "fill_waterbodies": True, "fill_continents": True, "draw_flighttrack": True, "label_flighttrack": True, "colour_water": ((153 / 255.), (255 / 255.), (255 / 255.), (255 / 255.)), "colour_land": ((204 / 255.), (153 / 255.), (102 / 255.), (255 / 255.)), "colour_ft_vertices": (0, 0, 1, 1), "colour_ft_waypoints": (1, 0, 0, 1) } if settings_dict is not None: settings.update(settings_dict) self.appearance_settings = settings if self.map is not None: self.map.set_graticule_visible(settings["draw_graticule"]) self.map.set_coastlines_visible(settings["draw_coastlines"]) self.map.set_fillcontinents_visible( visible=settings["fill_continents"], land_color=settings["colour_land"], lake_color=settings["colour_water"]) self.map.set_mapboundary_visible( visible=settings["fill_waterbodies"], bg_color=settings["colour_water"]) self.waypoints_interactor.set_path_color( line_color=settings["colour_ft_vertices"], marker_facecolor=settings["colour_ft_waypoints"]) self.waypoints_interactor.set_vertices_visible( settings["draw_flighttrack"]) self.waypoints_interactor.set_labels_visible( settings["label_flighttrack"]) def set_remote_sensing_appearance(self, settings): self.waypoints_interactor.set_remote_sensing(settings["reference"]) self.waypoints_interactor.set_tangent_visible( settings["draw_tangents"]) self.waypoints_interactor.set_solar_angle_visible( settings["show_solar_angle"]) self.waypoints_interactor.redraw_path() def get_map_appearance(self): """ """ return self.appearance_settings
class MSColabVersionHistory(QtWidgets.QMainWindow, ui.Ui_MscolabVersionHistory): """Derives QMainWindow to provide some common functionality to all MSUI view windows. """ name = "MSColab Version History Window" identifier = None viewCloses = QtCore.pyqtSignal(name="viewCloses") reloadWindows = QtCore.pyqtSignal(name="reloadWindows") def __init__(self, token, p_id, user, project_name, conn, parent=None, mscolab_server_url=config_loader( dataset="default_MSCOLAB", default=mss_default.default_MSCOLAB)): """ token: access_token p_id: project id user: logged in user project_name: name of project, conn: socket connection parent: parent of widget mscolab_server_url: server url of mscolab """ super(MSColabVersionHistory, self).__init__(parent) self.setupUi(self) # Initialise Variables self.token = token self.p_id = p_id self.user = user self.project_name = project_name self.conn = conn self.mscolab_server_url = mscolab_server_url # Event handlers self.refreshBtn.clicked.connect(self.handle_refresh) self.checkoutBtn.clicked.connect(self.handle_undo) self.nameVersionBtn.clicked.connect(self.handle_named_version) self.deleteVersionNameBtn.clicked.connect( self.handle_delete_version_name) self.versionFilterCB.currentIndexChanged.connect( lambda: self.load_all_changes()) self.changes.currentItemChanged.connect(self.preview_change) # Setup UI self.deleteVersionNameBtn.setVisible(False) self.set_label_text() self.set_change_list_style() self.toggle_version_buttons(False) self.load_current_waypoints() self.load_all_changes() def set_label_text(self): self.usernameLabel.setText(f"Logged in: {self.user['username']}") self.projectNameLabel.setText(f"Project: {self.project_name}") def set_change_list_style(self): palette = self.changes.palette() self.changes.setStyleSheet(f""" QListWidget::item {{ border-bottom: 1px solid #222; }} QListWidget::item:selected {{ background-color: {palette.highlight().color().name()}; color: {palette.highlightedText().color().name()}; }} """) def toggle_version_buttons(self, state): self.checkoutBtn.setEnabled(state) self.nameVersionBtn.setEnabled(state) def load_current_waypoints(self): data = {"token": self.token, "p_id": self.p_id} url = url_join(self.mscolab_server_url, 'get_project') res = requests.get(url, data=data) xml_content = json.loads(res.text)["content"] waypoint_model = WaypointsTableModel(name="Current Waypoints", xml_content=xml_content) self.currentWaypointsTable.setModel(waypoint_model) def load_all_changes(self): """ get changes from api, clear listwidget, render them to ui """ data = {"token": self.token, "p_id": self.p_id} named_version_only = None if self.versionFilterCB.currentIndex() == 0: named_version_only = True query_string = url_encode({"named_version": named_version_only}) url_path = f'get_all_changes?{query_string}' url = url_join(self.mscolab_server_url, url_path) r = requests.get(url, data=data) changes = json.loads(r.text)["changes"] self.changes.clear() for change in changes: created_at = datetime.strptime(change["created_at"], "%Y-%m-%d, %H:%M:%S") local_time = utc_to_local_datetime(created_at) date = local_time.strftime('%d/%m/%Y') time = local_time.strftime('%I:%M %p') item_text = f'{change["username"]} made change on {date} at {time}' if change["version_name"] is not None: item_text = f'{change["version_name"]}\n{item_text}' item = QtWidgets.QListWidgetItem(item_text, parent=self.changes) item.id = change["id"] item.version_name = change["version_name"] self.changes.addItem(item) def preview_change(self, current_item, previous_item): font = QtGui.QFont() if previous_item is not None: previous_item.setFont(font) if current_item is None: self.changePreviewTable.setModel(None) self.deleteVersionNameBtn.setVisible(False) self.toggle_version_buttons(False) return font.setBold(True) current_item.setFont(font) data = {"token": self.token, "ch_id": current_item.id} url = url_join(self.mscolab_server_url, 'get_change_content') res = requests.get(url, data=data).json() waypoint_model = WaypointsTableModel(xml_content=res["content"]) self.changePreviewTable.setModel(waypoint_model) if current_item.version_name is not None: self.deleteVersionNameBtn.setVisible(True) else: self.deleteVersionNameBtn.setVisible(False) self.toggle_version_buttons(True) def request_set_version_name(self, version_name, ch_id): data = { "token": self.token, "version_name": version_name, "ch_id": ch_id, "p_id": self.p_id } url = url_join(self.mscolab_server_url, 'set_version_name') res = requests.post(url, data=data) return res def handle_named_version(self): version_name, completed = QtWidgets.QInputDialog.getText( self, 'Version Name Dialog', 'Enter version name:') if completed is True: if len(version_name) > 255 or len(version_name) == 0: show_popup(self, "Error", "Version name length has to be between 1 and 255") return selected_item = self.changes.currentItem() res = self.request_set_version_name(version_name, selected_item.id) res = res.json() if res["success"] is True: item_text = selected_item.text().split('\n')[-1] new_text = f"{version_name}\n{item_text}" selected_item.setText(new_text) selected_item.version_name = version_name self.deleteVersionNameBtn.setVisible(True) else: show_popup(self, "Error", res["message"]) def handle_delete_version_name(self): selected_item = self.changes.currentItem() res = self.request_set_version_name(None, selected_item.id) res = res.json() if res["success"] is True: # Remove item if the filter is set to Named version if self.versionFilterCB.currentIndex() == 0: self.changes.takeItem(self.changes.currentRow()) # Remove name from item else: item_text = selected_item.text().split('\n')[-1] selected_item.setText(item_text) selected_item.version_name = None self.deleteVersionNameBtn.setVisible(False) else: show_popup(self, "Error", res["message"]) def handle_undo(self): qm = QtWidgets.QMessageBox ret = qm.question(self, self.tr("Undo"), "Do you want to checkout to this change?", qm.Yes, qm.No) if ret == qm.Yes: data = { "token": self.token, "ch_id": self.changes.currentItem().id } url = url_join(self.mscolab_server_url, 'undo') r = requests.post(url, data=data) if r.text == "True": # reload windows self.reloadWindows.emit() self.load_current_waypoints() self.load_all_changes() def handle_refresh(self): self.load_current_waypoints() self.load_all_changes() def closeEvent(self, event): self.viewCloses.emit()
class ConnectionManager(QtCore.QObject): signal_reload = QtCore.Signal(int, name="reload_wps") signal_message_receive = QtCore.Signal(str, name="message rcv") signal_message_reply_receive = QtCore.Signal(str, name="message reply") signal_message_edited = QtCore.Signal(str, name="message editted") signal_message_deleted = QtCore.Signal(str, name="message deleted") signal_new_permission = QtCore.Signal(int, int, name="new permission") signal_update_permission = QtCore.Signal(int, int, str, name="update permission") signal_revoke_permission = QtCore.Signal(int, int, name="revoke permission") signal_project_permissions_updated = QtCore.Signal( int, name="project permissions updated") signal_project_deleted = QtCore.Signal(int, name="project deleted") def __init__(self, token, user, mscolab_server_url=mss_default.mscolab_server_url): super(ConnectionManager, self).__init__() self.token = token self.user = user self.mscolab_server_url = mscolab_server_url self.sio = socketio.Client(reconnection_attempts=5) self.sio.connect(self.mscolab_server_url) self.sio.on('file-changed', handler=self.handle_file_change) # on chat message recive self.sio.on('chat-message-client', handler=self.handle_incoming_message) self.sio.on('chat-message-reply-client', handler=self.handle_incoming_message_reply) # on message edit self.sio.on('edit-message-client', handler=self.handle_message_edited) # on message delete self.sio.on('delete-message-client', handler=self.handle_message_deleted) # on new permission self.sio.on('new-permission', handler=self.handle_new_permission) # on update of permission self.sio.on('update-permission', handler=self.handle_update_permission) # on revoking project permission self.sio.on('revoke-permission', handler=self.handle_revoke_permission) # on updating project permissions in admin window self.sio.on('project-permissions-updated', handler=self.handle_project_permissions_updated) # On Project Delete self.sio.on('project-deleted', handler=self.handle_project_deleted) self.sio.emit('start', {'token': token}) def handle_update_permission(self, message): """ signal update of permission affected """ message = json.loads(message) p_id = int(message["p_id"]) u_id = int(message["u_id"]) access_level = message["access_level"] self.signal_update_permission.emit(p_id, u_id, access_level) def handle_new_permission(self, message): """ signal updating of newly added permission """ message = json.loads(message) p_id = int(message["p_id"]) u_id = int(message["u_id"]) self.signal_new_permission.emit(p_id, u_id) def handle_revoke_permission(self, message): """ Signal update of revoked permission """ message = json.loads(message) p_id = int(message["p_id"]) u_id = int(message["u_id"]) self.signal_revoke_permission.emit(p_id, u_id) def handle_project_permissions_updated(self, message): message = json.loads(message) u_id = int(message["u_id"]) self.signal_project_permissions_updated.emit(u_id) def handle_incoming_message(self, message): # raise signal to render to view logging.debug(message) # emit signal self.signal_message_receive.emit(message) def handle_incoming_message_reply(self, message): self.signal_message_reply_receive.emit(message) def handle_message_edited(self, message): self.signal_message_edited.emit(message) def handle_message_deleted(self, message): self.signal_message_deleted.emit(message) def handle_file_change(self, message): message = json.loads(message) self.signal_reload.emit(message["p_id"]) def handle_project_deleted(self, message): p_id = int(json.loads(message)["p_id"]) self.signal_project_deleted.emit(p_id) def handle_new_room(self, p_id): logging.debug("adding user to new room") self.sio.emit('add-user-to-room', {"p_id": p_id, "token": self.token}) def send_message(self, message_text, p_id, reply_id): logging.debug("sending message") self.sio.emit( 'chat-message', { "p_id": p_id, "token": self.token, "message_text": message_text, "reply_id": reply_id }) def edit_message(self, message_id, new_message_text, p_id): self.sio.emit( 'edit-message', { "message_id": message_id, "new_message_text": new_message_text, "p_id": p_id, "token": self.token }) def delete_message(self, message_id, p_id): self.sio.emit('delete-message', { 'message_id': message_id, 'p_id': p_id, 'token': self.token }) def save_file(self, token, p_id, content, comment=None): logging.debug("saving file") self.sio.emit( 'file-save', { "p_id": p_id, "token": self.token, "content": content, "comment": comment }) def disconnect(self): self.sio.disconnect()
class MSSViewWindow(QtWidgets.QMainWindow): """Derives QMainWindow to provide some common functionality to all MSUI view windows. """ name = "Abstract MSS View Window" identifier = None viewCloses = QtCore.pyqtSignal(name="viewCloses") # views for mscolab viewClosesId = QtCore.Signal(int, name="viewClosesId") def __init__(self, parent=None, model=None, _id=None): super(MSSViewWindow, self).__init__(parent) # Object variables: self.waypoints_model = model # pointer to the current flight track. # List that accommodates the dock window instances: Needs to be defined # in proper size in derived classes! self.docks = [] # emit _id if not none logging.debug(_id) self._id = _id # Used to force close window without the dialog popping up self.force_close = False def handle_force_close(self): self.force_close = True self.close() def closeEvent(self, event): """ if force_close is True then close window without dialog else ask user if he/she wants to close the window. Overloads QtGui.QMainWindow.closeEvent(). This method is called if Qt receives a window close request for our application window. """ if self.force_close is True: event.accept() return ret = QtWidgets.QMessageBox.warning( self, self.tr("Mission Support System"), self.tr("Do you want to close this {}?".format(self.name)), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if ret == QtWidgets.QMessageBox.Yes: self.viewCloses.emit() if self._id is not None: self.viewClosesId.emit(self._id) logging.debug(self._id) event.accept() else: event.ignore() def setFlightTrackModel(self, model): """Set the QAbstractItemModel instance that the view displays. """ self.waypoints_model = model def controlToBeCreated(self, index): """Check if the dock widget at index <index> exists. If yes, show the widget and return -1. Otherwise return <index-1>. """ index -= 1 if index >= 0 and self.docks[index] is not None: # The widget has already been created, but is not visible at # the moment. self.docks[index].show() self.docks[index].raise_() index = -1 if hasattr(self, "cbTools"): self.cbTools.setCurrentIndex(0) return index def createDockWidget(self, index, title, widget): """Create a new dock widget. A pointer to the dock widget will be stored in self.docks[index]. The dock will have the title <title> and contain the Qt widget <widget>. """ self.docks[index] = QtWidgets.QDockWidget(title, self) self.docks[index].setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) # setWidget transfers the widget's ownership to Qt -- no setParent() # call is necessary: self.docks[index].setWidget(widget) self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.docks[index]) # Check if another dock widget occupies the dock area. If yes, # tabbify the old and the new widget. for dock in self.docks: if dock and not dock == self.docks[index] and not dock.isFloating( ): self.tabifyDockWidget(dock, self.docks[index]) break self.docks[index].show() self.docks[index].raise_() @abstractmethod def getView(self): """Return view object that tools can interact with. ABSTRACT method, needs to be implemented in derived classes. """ return None def setIdentifier(self, identifier): self.identifier = identifier