class Exercice(Panel_API_graphique): titre = u"Exercice" # À adapter pour chaque module def __init__(self, *args, **kw): Panel_API_graphique.__init__(self, *args, **kw) self.entrees = QVBoxLayout() self.entrees.addSpacing(30) self.panneau = QLabel('') self.entrees.addWidget(self.panneau) self.entrees.addStretch() self.felicitations = QLabel('') self.entrees.addWidget(self.felicitations) self.entrees.addSpacing(30) self.btn_niveau = QPushButton(u"Niveau suivant", self) self.btn_niveau.clicked.connect(self.niveau_suivant) self.entrees.addWidget(self.btn_niveau) self.entrees.addSpacing(50) self.sizer = QHBoxLayout() self.sizer.addWidget(self.canvas, 1) self.sizer.addLayout(self.entrees, 0.2) self.finaliser(contenu=self.sizer) self.reinitialiser() def reinitialiser(self): u"""Revient au 1er niveau, et remet tous les réglages par défaut. Chaque niveau peut bien sûr modifier ces réglages. Quelques remarques: * le clic droit est désactivé, car il permet d'obtenir la réponse en éditant les propriétés du champ de texte. * l'édition des champs/textes avec [Entrée] est désactivée (car cela s'est avéré perturber les élèves). """ # Ne pas éditer les champs/textes avec [Entrée] self.canvas.editeur.active = False # Ne pas éditer les objets par un clic droit self.canvas.edition_par_clic_droit = False # Réglages par défaut self.canvas.fixe = True self.canvas.afficher_axes = False self.canvas.afficher_quadrillage = False self.afficher_barre_outils(False) self.canvas.ratio = None # Réinitialisation du score et retour au niveau 1 if param.debug: print(u'Module %s: réinitialisation...' % self.nom) self.score = 0 self.niveau = 0 self.erreurs = 0 self.niveau_suivant() def niveau_suivant(self, niveau=None): # On ferme toutes les feuilles ouvertes (inutile en principe), # et on en ouvre une nouvelle. self.fermer_feuilles() # Et on change de niveau... if niveau in (None, False): # None ou False (False est renvoyé par Qt via QAbstractBouton.clicked) self.niveau += 1 else: self.niveau = niveau if param.debug: print("== Niveau %s ==" % self.niveau) getattr(self, 'niveau%s' % self.niveau)() self.btn_niveau.setEnabled(False) self.felicitations.setStyleSheet( """QLabel {background-color: white; padding: 5px; border-radius: 5px; color:white;}""") self.update_panneau() n = niveau_suivant def update_panneau(self): self.panneau.setStyleSheet( """QLabel { padding: 10px; border-width: 2px; border-style:solid; border-radius: 5px; border-color:%s; background-color: %s }""" %(QColor(30, 144, 255).name(), QColor(176, 226, 255).name()) ) self.panneau.setText((u"<p><b><i>Niveau :</i> %s</b></p>" % self.niveau) + (u"<p><b><i>Points :</i> %s</b></p>" % self.score) + (u"<p><i>Erreurs :</i> %s</p>" % self.erreurs)) champs = self.feuille_actuelle.objets.lister(type=Champ) if champs and all(obj.correct for obj in champs): if hasattr(self, 'niveau' + str(self.niveau + 1)): self.btn_niveau.setEnabled(True) self.btn_niveau.setFocus(True) self.felicitations.setText(u'<p><b>Félicitations !</b></p>' + u'<p>Passer au niveau %s</p>' %(self.niveau + 1)) self.felicitations.setStyleSheet( """QLabel {background-color: %s; padding: 5px; border-radius: 5px; color:white;}""" %QColor(255, 153, 0).name()) else: self.felicitations.setText(u'<p><b>Félicitations !</b></p>' + u'<p>Dernier niveau terminé !</p>') self.felicitations.setStyleSheet( """QLabel {background-color: %s; padding: 5px; border-radius: 5px; color:white;}""" %QColor(102, 205, 0).name()) ##def _sauvegarder(self, fgeo, feuille = None): ##Panel_API_graphique._sauvegarder(self, fgeo, feuille) ##fgeo.contenu[u"niveau"] = [str(self.niveau)] ##fgeo.contenu[u"expression"] = [self.raw_expression] ##fgeo.contenu[u"score"] = [str(self.score)] ##fgeo.contenu[u"erreurs"] = [str(self.erreurs)] ## def _ouvrir(self, fgeo): pass ### Il ne doit y avoir qu'une seule feuille ouverte à la fois. ### XXX: intégrer cette fonctionnalité directement au Panel. ##self.fermer_feuilles() ##Panel_API_graphique._ouvrir(self, fgeo) ##if fgeo.contenu.has_key(u"expression"): ##self.generer_expression(expr=fgeo.contenu[u"expression"][0]) ##self.dessiner_tableau() ##if fgeo.contenu.has_key(u"niveau"): ##self.niveau = int(fgeo.contenu[u"niveau"][0]) ##if fgeo.contenu.has_key(u"score"): ##self.score = int(fgeo.contenu[u"score"][0]) ##if fgeo.contenu.has_key(u"erreurs"): ##self.erreurs = int(fgeo.contenu[u"erreurs"][0]) ##self.update_panneau() ##def _affiche(self): ##self.dessiner_tableau() # -------------------------------- # Génération de nombres aléatoires # -------------------------------- @staticmethod def signe(): return 2*randint(0, 1) - 1 def naturel(self, n=15): u'''Retourne un entier entre 2 et `n`.''' return randint(2, n) def relatif(self, n=15): u'''Retourne un entier entre -`n` et -2, ou entre 2 et `n`.''' # signe: 1 ou -1 return self.signe()*self.naturel(n) def decimal(self, chiffres=2): u'''Retourne un nombre décimal, `chiffres` est le nombre de chiffres.''' return S('%s.%s' % (self.relatif(), self.naturel())) def rationnel(self, n=7): u'''Retourne un quotient d'entiers.''' while True: p = self.naturel(n) q = self.naturel(n) if p%q: break return self.signe()*S(p)/S(q) def couple(self, m=7, n=7): u"""Retourne un couple d'entiers relatifs.""" return self.relatif(m), self.relatif(n) def autocompleter(self): u"""Compléter automatiquement avec les bonnes réponses pour pouvoir passer au niveau suivant. Essentiellement pour déboguer.""" ##if self.btn_niveau.isEnabled(): ##self.niveau_suivant() self.btn_niveau.click() for t in self.feuille_actuelle.objets.lister(type=Champ): t.texte = t.style('attendu') t.style(color='g') ##self.parent.parent.ligne_commande.setFocus() a = property(autocompleter) def bonus(self, expr): u"""À surclasser pour accorder un bonus si l'expression vérifie certaines conditions. Par exemple, on peut tester que le résultat est bien simplifié.""" return False def compter_points(self, **kw): if 'correct' in kw and 'correct_old' in kw and 'champ' in kw: champ = kw['champ'] if kw['correct']: if not kw['correct_old']: if not champ.style('choix'): # C'est plus dur s'il n'y a pas de choix proposé. # On accorde une bonification si le résultat est # un minimum simplifié. if self.bonus(champ.label()): self.score += 3 self.score += 1 self.score += 1 else: self.score -= 1 self.erreurs += 1 if all(obj.correct for obj in self.feuille_actuelle.objets.lister(type=Champ)): self.score += 10*(self.niveau + 1) self.update_panneau()
class ContentsBrowser(QDialog): """ Displays the contents of a DVID server, listing all repos and the volumes/nodes within each repo. The user's selected repo, volume, and node can be accessed via the `get_selection()` method. If the dialog is constructed with mode='specify_new', then the user is asked to provide a new data name, and choose the repo and node to which it will belong. **TODO:** * Show more details in node list (e.g. date modified, parents, children) * Gray-out nodes that aren't "open" for adding new volumes """ def __init__(self, suggested_hostnames, default_nodes=None, mode='select_existing', selectable_type=VOXEL_TYPENAMES, parent=None): """ Constructor. suggested_hostnames: A list of hostnames to suggest to the user, e.g. ["localhost:8000"] default_nodes: A dict of {hostname : uuid} specifying which node to auto-select for each possible host. mode: Either 'select_existing' or 'specify_new' parent: The parent widget. """ assert mode in ('select_existing', 'specify_new'), "Invalid mode: {}".format(mode) super(ContentsBrowser, self).__init__(parent) self._suggested_hostnames = suggested_hostnames self._default_nodes = default_nodes self._mode = mode self._current_repo = None self._repos_info = None self._hostname = None if isinstance(selectable_type, collections.Iterable): self._selectable_types = selectable_type else: assert isinstance( selectable_type, str), "selectable_type must be str or list-of-str" self._selectable_types = (selectable_type, ) # Create the UI self._init_layout() VolumeSelection = collections.namedtuple( "VolumeSelection", "hostname repo_uuid data_name node_uuid typename") def get_selection(self): """ Get the user's current (or final) selection. Returns a VolumeSelection tuple. """ # Convert to str (Qt tends to convert these into unicode) node_uuid = str(self._get_selected_node()) repo_uuid, data_name, typename = map(str, self._get_selected_data()) if self._mode == "specify_new": data_name = str(self._new_data_edit.text()) return ContentsBrowser.VolumeSelection(str(self._hostname), repo_uuid, data_name, node_uuid, typename) def _init_layout(self): """ Create the GUI widgets (but leave them empty). """ hostname_combobox = QComboBox(parent=self) self._hostname_combobox = hostname_combobox hostname_combobox.setEditable(True) hostname_combobox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) for hostname in self._suggested_hostnames: hostname_combobox.addItem(hostname) # EventFilter is installed after everything else is initialized. (See below.) #hostname_combobox.installEventFilter(self) self._connect_button = QPushButton("Connect", parent=self, clicked=self._handle_new_hostname) hostname_layout = QHBoxLayout() hostname_layout.addWidget(hostname_combobox) hostname_layout.addWidget(self._connect_button) hostinfo_table = QTableWidget() hostinfo_table.setColumnCount(len(SERVER_INFO_FIELDS)) hostinfo_table.setHorizontalHeaderLabels(SERVER_INFO_FIELDS) hostinfo_table.horizontalHeader().setVisible(True) hostinfo_table.verticalHeader().setVisible(False) hostinfo_table.setRowCount(1) hostinfo_table.setItem(0, 0, QTableWidgetItem("Placeholder")) hostinfo_table.setVisible(False) hostinfo_table.resizeRowsToContents() hostinfo_table.horizontalHeader().setStretchLastSection(True) table_height = hostinfo_table.verticalHeader().sectionSize( 0) + hostinfo_table.rowHeight(0) hostinfo_table.resize(QSize(hostinfo_table.width(), table_height)) hostinfo_table.setMaximumSize(QSize(1000, table_height)) hostinfo_table.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) host_layout = QVBoxLayout() host_layout.addLayout(hostname_layout) host_layout.addWidget(hostinfo_table) host_groupbox = QGroupBox("DVID Host", parent=self) host_groupbox.setLayout(host_layout) host_groupbox.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) repo_treewidget = QTreeWidget(parent=self) repo_treewidget.setHeaderLabels( TREEVIEW_COLUMNS) # TODO: Add type, shape, axes, etc. repo_treewidget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) repo_treewidget.itemSelectionChanged.connect( self._handle_data_selection) data_layout = QVBoxLayout() data_layout.addWidget(repo_treewidget) data_groupbox = QGroupBox("Data Volumes", parent=self) data_groupbox.setLayout(data_layout) node_listwidget = QListWidget(parent=self) node_listwidget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) node_listwidget.itemSelectionChanged.connect(self._update_status) node_layout = QVBoxLayout() node_layout.addWidget(node_listwidget) node_groupbox = QGroupBox("Nodes", parent=self) node_groupbox.setLayout(node_layout) new_data_edit = QLineEdit(parent=self) new_data_edit.textEdited.connect(self._update_status) full_url_label = QLabel(parent=self) full_url_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) text_flags = full_url_label.textInteractionFlags() full_url_label.setTextInteractionFlags(text_flags | Qt.TextSelectableByMouse) new_data_layout = QVBoxLayout() new_data_layout.addWidget(new_data_edit) new_data_groupbox = QGroupBox("New Data Volume", parent=self) new_data_groupbox.setLayout(new_data_layout) new_data_groupbox.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) buttonbox = QDialogButtonBox(Qt.Horizontal, parent=self) buttonbox.setStandardButtons(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttonbox.accepted.connect(self.accept) buttonbox.rejected.connect(self.reject) buttonbox.button(QDialogButtonBox.Ok).setEnabled(False) layout = QVBoxLayout() layout.addWidget(host_groupbox) layout.addWidget(data_groupbox) layout.addWidget(node_groupbox) if self._mode == "specify_new": layout.addWidget(new_data_groupbox) else: new_data_groupbox.hide() layout.addWidget(full_url_label) layout.addWidget(buttonbox) # Stretch factors layout.setStretchFactor(data_groupbox, 3) layout.setStretchFactor(node_groupbox, 1) self.setLayout(layout) self.setWindowTitle("Select DVID Volume") self.resize(1000, 1000) # Initially disabled data_groupbox.setEnabled(False) node_groupbox.setEnabled(False) new_data_groupbox.setEnabled(False) # Set tab order self.setTabOrder(hostname_combobox, repo_treewidget) self.setTabOrder(repo_treewidget, node_listwidget) self.setTabOrder(node_listwidget, buttonbox) # Save instance members self._hostinfo_table = hostinfo_table self._data_groupbox = data_groupbox self._node_groupbox = node_groupbox self._new_data_groupbox = new_data_groupbox self._repo_treewidget = repo_treewidget self._node_listwidget = node_listwidget self._new_data_edit = new_data_edit self._full_url_label = full_url_label self._buttonbox = buttonbox # Finally install eventfilter (after everything is initialized) hostname_combobox.installEventFilter(self) def sizeHint(self): return QSize(1000, 1000) def eventFilter(self, watched, event): """ When the user presses the 'Enter' key, auto-click 'Connect'. If we've already connected, auto-click 'OK'. """ if watched == self._hostname_combobox \ and event.type() == QEvent.KeyPress \ and ( event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter): if self._hostname: self._buttonbox.button(QDialogButtonBox.Ok).click() else: self._connect_button.click() return True return False def showEvent(self, event): """ Raise the window when it is shown. For some reason, that doesn't happen automatically if this widget is also the main window. """ super(ContentsBrowser, self).showEvent(event) self.raise_() def _handle_new_hostname(self): """ Called by 'Connect' button. Connect to the server, download the server info and repo info, and populate the GUI widgets with the data. """ new_hostname = str(self._hostname_combobox.currentText()) if '://' in new_hostname: new_hostname = new_hostname.split('://')[1] error_msg = None self._server_info = None self._repos_info = None self._current_repo = None self._hostname = None try: # Query the server connection = DVIDConnection(new_hostname) self._server_info = ContentsBrowser._get_server_info(connection) self._repos_info = ContentsBrowser._get_repos_info(connection) self._hostname = new_hostname self._connection = connection except DVIDException as ex: error_msg = "libdvid.DVIDException: {}".format(ex.message) except ErrMsg as ex: error_msg = "libdvid.ErrMsg: {}".format(ex.message) if error_msg: QMessageBox.critical(self, "Connection Error", error_msg) self._populate_node_list(None) else: self._connect_button.setEnabled(False) self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(True) enable_contents = self._repos_info is not None self._data_groupbox.setEnabled(enable_contents) self._node_groupbox.setEnabled(enable_contents) self._new_data_groupbox.setEnabled(enable_contents) self._populate_hostinfo_table() self._populate_repo_tree() @classmethod def _get_server_info(cls, connection): status, body, _error_message = connection.make_request( "/server/info", ConnectionMethod.GET) server_info = json.loads(body) return server_info @classmethod def _get_repos_info(cls, connection): status, body, _error_message = connection.make_request( "/repos/info", ConnectionMethod.GET) repos_info = json.loads(body) # Discard uuids with 'null' content (I don't know why they sometimes exist...) repos_info = [ uuid_repo_info for uuid_repo_info in list(repos_info.items()) if uuid_repo_info[1] ] return collections.OrderedDict(sorted(repos_info)) def _populate_hostinfo_table(self): self._hostinfo_table.setVisible(self._server_info is not None) if not self._server_info: return for column_index, fieldname in enumerate(SERVER_INFO_FIELDS): try: field = self._server_info[fieldname] except KeyError: field = "<UNDEFINED>" item = QTableWidgetItem(field) flags = item.flags() flags &= ~Qt.ItemIsSelectable flags &= ~Qt.ItemIsEditable item.setFlags(flags) self._hostinfo_table.setItem(0, column_index, item) self._hostinfo_table.resizeColumnsToContents() self._hostinfo_table.horizontalHeader().setStretchLastSection( True) # Force refresh of last column. def _populate_repo_tree(self): """ Initialize the tree widget of repos and volumes. """ self._repo_treewidget.clear() if self._repos_info is None: return for repo_uuid, repo_info in sorted(self._repos_info.items()): if repo_info is None: continue repo_column_dict = collections.defaultdict(str) repo_column_dict["Alias"] = repo_info["Alias"] repo_column_dict["Details"] = "Created: " + repo_info["Created"] repo_column_dict["UUID"] = repo_uuid repo_column_values = [ repo_column_dict[k] for k in TREEVIEW_COLUMNS ] repo_item = QTreeWidgetItem(self._repo_treewidget, QStringList(repo_column_values)) repo_item.setData(0, Qt.UserRole, (repo_uuid, "", "")) for data_name, data_info in repo_info["DataInstances"].items(): data_instance_dict = collections.defaultdict(str) data_instance_dict["Alias"] = data_name typename = data_info["Base"]["TypeName"] data_instance_dict["TypeName"] = typename is_voxels = (typename in VOXEL_TYPENAMES) if is_voxels: start_coord = data_info["Extended"]["MinPoint"] if start_coord: start_coord = tuple(start_coord) stop_coord = data_info["Extended"]["MaxPoint"] if stop_coord: stop_coord = tuple(x + 1 for x in stop_coord) if start_coord and stop_coord: shape = tuple(b - a for a, b in zip(start_coord, stop_coord)) else: shape = None data_instance_dict[ "Details"] = "Size={} | Start={} | Stop={}".format( shape, start_coord, stop_coord) data_column_values = [ data_instance_dict[k] for k in TREEVIEW_COLUMNS ] data_item = QTreeWidgetItem(repo_item, data_column_values) data_item.setData(0, Qt.UserRole, (repo_uuid, data_name, typename)) # If we're in specify_new mode, only the # repo parent items are selectable. # Also, non-volume items aren't selectable. if self._mode == 'specify_new' or typename not in self._selectable_types: flags = data_item.flags() flags &= ~Qt.ItemIsSelectable flags &= ~Qt.ItemIsEnabled data_item.setFlags(flags) self._repo_treewidget.collapseAll() self._repo_treewidget.setSortingEnabled(True) if self._hostname in self._default_nodes: self._select_node_uuid(self._default_nodes[self._hostname]) self._repo_treewidget.resizeColumnToContents(0) self._repo_treewidget.setFocus() def _handle_data_selection(self): """ When the user clicks a new data item, respond by updating the node list. """ selected_items = self._repo_treewidget.selectedItems() if not selected_items: return None item = selected_items[0] item_data = item.data(0, Qt.UserRole).toPyObject() if not item_data: return repo_uuid, data_name, typename = item_data if self._current_repo != repo_uuid: self._populate_node_list(repo_uuid) self._update_status() def _populate_node_list(self, repo_uuid): """ Replace the contents of the node list widget to show all the nodes for the currently selected repo. """ self._node_listwidget.clear() if self._repos_info is None or repo_uuid is None: return # For now, we simply show the nodes in sorted order, without respect to dag order all_uuids = sorted(self._repos_info[repo_uuid]["DAG"]["Nodes"].keys()) for node_uuid in all_uuids: node_item = QListWidgetItem(node_uuid, parent=self._node_listwidget) node_item.setData(Qt.UserRole, node_uuid) self._current_repo = repo_uuid # Select the last one by default. last_row = self._node_listwidget.count() - 1 last_item = self._node_listwidget.item(last_row) self._node_listwidget.setCurrentItem(last_item) self._update_status() def _get_selected_node(self): """ Return the currently selected node uuid. """ selected_items = self._node_listwidget.selectedItems() if not selected_items: return None selected_node_item = selected_items[0] node_item_data = selected_node_item.data(Qt.UserRole) return str(node_item_data.toString()) def _get_selected_data(self): """ Return the repo, data name, and type of the currently selected data volume (or ROI). """ selected_items = self._repo_treewidget.selectedItems() if not selected_items: return None, None selected_data_item = selected_items[0] data_item_data = selected_data_item.data(0, Qt.UserRole).toPyObject() if selected_data_item: repo_uuid, data_name, typename = data_item_data else: repo_uuid = data_name = typename = None return repo_uuid, data_name, typename def _select_node_uuid(self, node_uuid): """ Locate the repo that owns this uuid, and select it in the GUI. If the uuid can't be found, do nothing. """ def select_repotree_item(repo_uuid): for row in range(self._repo_treewidget.topLevelItemCount()): repo_item = self._repo_treewidget.topLevelItem(row) if repo_uuid == repo_item.data(0, Qt.UserRole).toPyObject()[0]: self._repo_treewidget.setCurrentItem(repo_item) repo_item.setExpanded(True) self._repo_treewidget.scrollTo( self._repo_treewidget.selectedIndexes()[0], QTreeWidget.PositionAtCenter) break def select_nodelist_item(node_uuid): for row in range(self._node_listwidget.count()): item = self._node_listwidget.item(row) if node_uuid == item.data(Qt.UserRole).toPyObject(): self._node_listwidget.setCurrentItem(item) break for repo_uuid, repo_info in sorted(self._repos_info.items()): if node_uuid in repo_info["DAG"]["Nodes"].keys(): # Select the right repo parent item select_repotree_item(repo_uuid) # Select the right row in the node list # (The node list was automatically updated when the repo selection changed, above.) select_nodelist_item(node_uuid) break def _update_status(self): """ Update the path label to reflect the user's currently selected uuid and new volume name. """ hostname, repo_uuid, dataname, node_uuid, typename = self.get_selection( ) full_path = "http://{hostname}/api/node/{uuid}/{dataname}"\ "".format( hostname=self._hostname, uuid=node_uuid, dataname=dataname ) self._full_url_label.setText(full_path) ok_button = self._buttonbox.button(QDialogButtonBox.Ok) ok_button.setEnabled(dataname != "")
class ContentsBrowser(QDialog): """ Displays the contents of a DVID server, listing all datasets and the volumes/nodes within each dataset. The user's selected dataset, volume, and node can be accessed via the `get_selection()` method. If the dialog is constructed with mode='specify_new', then the user is asked to provide a new data name, and choose the dataset and node to which it will belong. **TODO:** * Show more details in dataset list (e.g. shape, axes, pixel type) * Show more details in node list (e.g. date modified, parents, children) * Gray-out nodes that aren't "open" for adding new volumes """ def __init__(self, suggested_hostnames, mode='select_existing', parent=None): """ Constructor. suggested_hostnames: A list of hostnames to suggest to the user, e.g. ["localhost:8000"] mode: Either 'select_existing' or 'specify_new' parent: The parent widget. """ super(ContentsBrowser, self).__init__(parent) self._suggested_hostnames = suggested_hostnames self._mode = mode self._current_dset = None self._repos_info = None self._hostname = None # Create the UI self._init_layout() VolumeSelection = collections.namedtuple( "VolumeSelection", "hostname dataset_uuid data_name node_uuid") def get_selection(self): """ Get the user's current (or final) selection. Returns a VolumeSelection tuple. """ node_uuid = self._get_selected_node() dset_uuid, data_name = self._get_selected_data() if self._mode == "specify_new": data_name = str(self._new_data_edit.text()) return ContentsBrowser.VolumeSelection(self._hostname, dset_uuid, data_name, node_uuid) def _init_layout(self): """ Create the GUI widgets (but leave them empty). """ hostname_combobox = QComboBox(parent=self) self._hostname_combobox = hostname_combobox hostname_combobox.setEditable(True) hostname_combobox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) hostname_combobox.installEventFilter(self) for hostname in self._suggested_hostnames: hostname_combobox.addItem(hostname) self._connect_button = QPushButton("Connect", parent=self, clicked=self._handle_new_hostname) hostname_layout = QHBoxLayout() hostname_layout.addWidget(hostname_combobox) hostname_layout.addWidget(self._connect_button) hostname_groupbox = QGroupBox("DVID Host", parent=self) hostname_groupbox.setLayout(hostname_layout) hostname_groupbox.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) data_treewidget = QTreeWidget(parent=self) data_treewidget.setHeaderLabels( ["Data"]) # TODO: Add type, shape, axes, etc. data_treewidget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) data_treewidget.itemSelectionChanged.connect( self._handle_data_selection) data_layout = QVBoxLayout() data_layout.addWidget(data_treewidget) data_groupbox = QGroupBox("Data Volumes", parent=self) data_groupbox.setLayout(data_layout) node_listwidget = QListWidget(parent=self) node_listwidget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) node_listwidget.itemSelectionChanged.connect(self._update_display) node_layout = QVBoxLayout() node_layout.addWidget(node_listwidget) node_groupbox = QGroupBox("Nodes", parent=self) node_groupbox.setLayout(node_layout) new_data_edit = QLineEdit(parent=self) new_data_edit.textEdited.connect(self._update_display) full_url_label = QLabel(parent=self) full_url_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) text_flags = full_url_label.textInteractionFlags() full_url_label.setTextInteractionFlags(text_flags | Qt.TextSelectableByMouse) new_data_layout = QVBoxLayout() new_data_layout.addWidget(new_data_edit) new_data_groupbox = QGroupBox("New Data Volume", parent=self) new_data_groupbox.setLayout(new_data_layout) new_data_groupbox.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) buttonbox = QDialogButtonBox(Qt.Horizontal, parent=self) buttonbox.setStandardButtons(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttonbox.accepted.connect(self.accept) buttonbox.rejected.connect(self.reject) buttonbox.button(QDialogButtonBox.Ok).setEnabled(False) layout = QVBoxLayout() layout.addWidget(hostname_groupbox) layout.addWidget(data_groupbox) layout.addWidget(node_groupbox) if self._mode == "specify_new": layout.addWidget(new_data_groupbox) else: new_data_groupbox.hide() layout.addWidget(full_url_label) layout.addWidget(buttonbox) # Stretch factors layout.setStretchFactor(data_groupbox, 3) layout.setStretchFactor(node_groupbox, 1) self.setLayout(layout) self.setWindowTitle("Select DVID Volume") # Initially disabled data_groupbox.setEnabled(False) node_groupbox.setEnabled(False) new_data_groupbox.setEnabled(False) # Save instance members self._data_groupbox = data_groupbox self._node_groupbox = node_groupbox self._new_data_groupbox = new_data_groupbox self._data_treewidget = data_treewidget self._node_listwidget = node_listwidget self._new_data_edit = new_data_edit self._full_url_label = full_url_label self._buttonbox = buttonbox def sizeHint(self): return QSize(700, 500) def eventFilter(self, watched, event): if watched == self._hostname_combobox \ and event.type() == QEvent.KeyPress \ and ( event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter): self._connect_button.click() return True return False def showEvent(self, event): """ Raise the window when it is shown. For some reason, that doesn't happen automatically if this widget is also the main window. """ super(ContentsBrowser, self).showEvent(event) self.raise_() def _handle_new_hostname(self): new_hostname = str(self._hostname_combobox.currentText()) if '://' in new_hostname: new_hostname = new_hostname.split('://')[1] error_msg = None self._repos_info = None self._current_dset = None self._hostname = None try: # Query the server connection = httplib.HTTPConnection(new_hostname) self._repos_info = pydvid.general.get_repos_info(connection) self._hostname = new_hostname self._connection = connection except socket.error as ex: error_msg = "Socket Error: {} (Error {})".format( ex.args[1], ex.args[0]) except httplib.HTTPException as ex: error_msg = "HTTP Error: {}".format(ex.args[0]) if error_msg: QMessageBox.critical(self, "Connection Error", error_msg) self._populate_datasets_tree() self._populate_node_list(-1) else: self._connect_button.setEnabled(False) self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(True) enable_contents = self._repos_info is not None self._data_groupbox.setEnabled(enable_contents) self._node_groupbox.setEnabled(enable_contents) self._new_data_groupbox.setEnabled(enable_contents) self._populate_datasets_tree() def _populate_datasets_tree(self): """ Initialize the tree widget of datasets and volumes. """ self._data_treewidget.clear() if self._repos_info is None: return for dset_uuid, dset_info in sorted(self._repos_info.items()): dset_item = QTreeWidgetItem(self._data_treewidget, QStringList(dset_uuid)) dset_item.setData(0, Qt.UserRole, (dset_uuid, "")) for data_name in dset_info["DataInstances"].keys(): data_item = QTreeWidgetItem(dset_item, QStringList(data_name)) data_item.setData(0, Qt.UserRole, (dset_uuid, data_name)) if self._mode == 'specify_new': # If we're in specify_new mode, only the dataset parent items are selectable. flags = data_item.flags() flags &= ~Qt.ItemIsSelectable flags &= ~Qt.ItemIsEnabled data_item.setFlags(flags) # Expand everything self._data_treewidget.expandAll() # Select the first item by default. if self._mode == "select_existing": first_item = self._data_treewidget.topLevelItem(0).child(0) else: first_item = self._data_treewidget.topLevelItem(0) self._data_treewidget.setCurrentItem(first_item, 0) def _handle_data_selection(self): """ When the user clicks a new data item, respond by updating the node list. """ selected_items = self._data_treewidget.selectedItems() if not selected_items: return None item = selected_items[0] item_data = item.data(0, Qt.UserRole).toPyObject() if not item_data: return dset_uuid, data_name = item_data if self._current_dset != dset_uuid: self._populate_node_list(dset_uuid) self._update_display() def _populate_node_list(self, dataset_uuid): """ Replace the contents of the node list widget to show all the nodes for the currently selected dataset. """ self._node_listwidget.clear() if self._repos_info is None: return # For now, we simply show the nodes in sorted order, without respect to dag order all_uuids = sorted( self._repos_info[dataset_uuid]["DAG"]["Nodes"].keys()) for node_uuid in all_uuids: node_item = QListWidgetItem(node_uuid, parent=self._node_listwidget) node_item.setData(Qt.UserRole, node_uuid) self._current_dset = dataset_uuid # Select the last one by default. last_row = self._node_listwidget.count() - 1 last_item = self._node_listwidget.item(last_row) self._node_listwidget.setCurrentItem(last_item) self._update_display() def _get_selected_node(self): selected_items = self._node_listwidget.selectedItems() if not selected_items: return None selected_node_item = selected_items[0] node_item_data = selected_node_item.data(Qt.UserRole) return str(node_item_data.toString()) def _get_selected_data(self): selected_items = self._data_treewidget.selectedItems() if not selected_items: return None, None selected_data_item = selected_items[0] data_item_data = selected_data_item.data(0, Qt.UserRole).toPyObject() if selected_data_item: dset_uuid, data_name = data_item_data else: dset_uuid = data_name = None return dset_uuid, data_name def _update_display(self): """ Update the path label to reflect the user's currently selected uuid and new volume name. """ hostname, dset_uuid, dataname, node_uuid = self.get_selection() full_path = "http://{hostname}/api/node/{uuid}/{dataname}"\ "".format( hostname=self._hostname, uuid=node_uuid, dataname=dataname ) self._full_url_label.setText(full_path) ok_button = self._buttonbox.button(QDialogButtonBox.Ok) ok_button.setEnabled(dataname != "")
class ContentsBrowser(QDialog): """ Displays the contents of a DVID server, listing all repos and the volumes/nodes within each repo. The user's selected repo, volume, and node can be accessed via the `get_selection()` method. If the dialog is constructed with mode='specify_new', then the user is asked to provide a new data name, and choose the repo and node to which it will belong. **TODO:** * Show more details in node list (e.g. date modified, parents, children) * Gray-out nodes that aren't "open" for adding new volumes """ def __init__(self, suggested_hostnames, default_nodes=None, mode='select_existing', selectable_type=VOXEL_TYPENAMES, parent=None): """ Constructor. suggested_hostnames: A list of hostnames to suggest to the user, e.g. ["localhost:8000"] default_nodes: A dict of {hostname : uuid} specifying which node to auto-select for each possible host. mode: Either 'select_existing' or 'specify_new' parent: The parent widget. """ assert mode in ('select_existing', 'specify_new'), "Invalid mode: {}".format(mode) super( ContentsBrowser, self ).__init__(parent) self._suggested_hostnames = suggested_hostnames self._default_nodes = default_nodes self._mode = mode self._current_repo = None self._repos_info = None self._hostname = None if isinstance(selectable_type, collections.Iterable): self._selectable_types = selectable_type else: assert isinstance(selectable_type, str), "selectable_type must be str or list-of-str" self._selectable_types = (selectable_type,) # Create the UI self._init_layout() VolumeSelection = collections.namedtuple( "VolumeSelection", "hostname repo_uuid data_name node_uuid typename" ) def get_selection(self): """ Get the user's current (or final) selection. Returns a VolumeSelection tuple. """ # Convert to str (Qt tends to convert these into unicode) node_uuid = str(self._get_selected_node()) repo_uuid, data_name, typename = map(str, self._get_selected_data()) if self._mode == "specify_new": data_name = str( self._new_data_edit.text() ) return ContentsBrowser.VolumeSelection(str(self._hostname), repo_uuid, data_name, node_uuid, typename) def _init_layout(self): """ Create the GUI widgets (but leave them empty). """ hostname_combobox = QComboBox(parent=self) self._hostname_combobox = hostname_combobox hostname_combobox.setEditable(True) hostname_combobox.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Maximum ) for hostname in self._suggested_hostnames: hostname_combobox.addItem( hostname ) # EventFilter is installed after everything else is initialized. (See below.) #hostname_combobox.installEventFilter(self) self._connect_button = QPushButton("Connect", parent=self, clicked=self._handle_new_hostname) hostname_layout = QHBoxLayout() hostname_layout.addWidget( hostname_combobox ) hostname_layout.addWidget( self._connect_button ) hostinfo_table = QTableWidget() hostinfo_table.setColumnCount(len(SERVER_INFO_FIELDS)) hostinfo_table.setHorizontalHeaderLabels(SERVER_INFO_FIELDS) hostinfo_table.horizontalHeader().setVisible(True) hostinfo_table.verticalHeader().setVisible(False) hostinfo_table.setRowCount(1) hostinfo_table.setItem(0,0, QTableWidgetItem("Placeholder")) hostinfo_table.setVisible(False) hostinfo_table.resizeRowsToContents() hostinfo_table.horizontalHeader().setStretchLastSection(True) table_height = hostinfo_table.verticalHeader().sectionSize(0) + hostinfo_table.rowHeight(0) hostinfo_table.resize( QSize( hostinfo_table.width(), table_height ) ) hostinfo_table.setMaximumSize( QSize( 1000, table_height ) ) hostinfo_table.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) host_layout = QVBoxLayout() host_layout.addLayout(hostname_layout) host_layout.addWidget(hostinfo_table) host_groupbox = QGroupBox("DVID Host", parent=self) host_groupbox.setLayout( host_layout ) host_groupbox.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred ) repo_treewidget = QTreeWidget(parent=self) repo_treewidget.setHeaderLabels( TREEVIEW_COLUMNS ) # TODO: Add type, shape, axes, etc. repo_treewidget.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred ) repo_treewidget.itemSelectionChanged.connect( self._handle_data_selection ) data_layout = QVBoxLayout() data_layout.addWidget( repo_treewidget ) data_groupbox = QGroupBox("Data Volumes", parent=self) data_groupbox.setLayout( data_layout ) node_listwidget = QListWidget(parent=self) node_listwidget.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred ) node_listwidget.itemSelectionChanged.connect( self._update_status ) node_layout = QVBoxLayout() node_layout.addWidget( node_listwidget ) node_groupbox = QGroupBox("Nodes", parent=self) node_groupbox.setLayout( node_layout ) new_data_edit = QLineEdit(parent=self) new_data_edit.textEdited.connect( self._update_status ) full_url_label = QLabel(parent=self) full_url_label.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Maximum ) text_flags = full_url_label.textInteractionFlags() full_url_label.setTextInteractionFlags( text_flags | Qt.TextSelectableByMouse ) new_data_layout = QVBoxLayout() new_data_layout.addWidget( new_data_edit ) new_data_groupbox = QGroupBox("New Data Volume", parent=self) new_data_groupbox.setLayout( new_data_layout ) new_data_groupbox.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Maximum ) buttonbox = QDialogButtonBox( Qt.Horizontal, parent=self ) buttonbox.setStandardButtons( QDialogButtonBox.Ok | QDialogButtonBox.Cancel ) buttonbox.accepted.connect( self.accept ) buttonbox.rejected.connect( self.reject ) buttonbox.button(QDialogButtonBox.Ok).setEnabled(False) layout = QVBoxLayout() layout.addWidget( host_groupbox ) layout.addWidget( data_groupbox ) layout.addWidget( node_groupbox ) if self._mode == "specify_new": layout.addWidget( new_data_groupbox ) else: new_data_groupbox.hide() layout.addWidget( full_url_label ) layout.addWidget( buttonbox ) # Stretch factors layout.setStretchFactor(data_groupbox, 3) layout.setStretchFactor(node_groupbox, 1) self.setLayout(layout) self.setWindowTitle( "Select DVID Volume" ) self.resize(1000, 1000) # Initially disabled data_groupbox.setEnabled(False) node_groupbox.setEnabled(False) new_data_groupbox.setEnabled(False) # Set tab order self.setTabOrder(hostname_combobox, repo_treewidget) self.setTabOrder(repo_treewidget, node_listwidget) self.setTabOrder(node_listwidget, buttonbox) # Save instance members self._hostinfo_table = hostinfo_table self._data_groupbox = data_groupbox self._node_groupbox = node_groupbox self._new_data_groupbox = new_data_groupbox self._repo_treewidget = repo_treewidget self._node_listwidget = node_listwidget self._new_data_edit = new_data_edit self._full_url_label = full_url_label self._buttonbox = buttonbox # Finally install eventfilter (after everything is initialized) hostname_combobox.installEventFilter(self) def sizeHint(self): return QSize(1000,1000) def eventFilter(self, watched, event): """ When the user presses the 'Enter' key, auto-click 'Connect'. If we've already connected, auto-click 'OK'. """ if watched == self._hostname_combobox \ and event.type() == QEvent.KeyPress \ and ( event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter): if self._hostname: self._buttonbox.button(QDialogButtonBox.Ok).click() else: self._connect_button.click() return True return False def showEvent(self, event): """ Raise the window when it is shown. For some reason, that doesn't happen automatically if this widget is also the main window. """ super(ContentsBrowser, self).showEvent(event) self.raise_() def _handle_new_hostname(self): """ Called by 'Connect' button. Connect to the server, download the server info and repo info, and populate the GUI widgets with the data. """ new_hostname = str( self._hostname_combobox.currentText() ) if '://' in new_hostname: new_hostname = new_hostname.split('://')[1] error_msg = None self._server_info = None self._repos_info = None self._current_repo = None self._hostname = None try: # Query the server connection = DVIDConnection(new_hostname) self._server_info = ContentsBrowser._get_server_info(connection) self._repos_info = ContentsBrowser._get_repos_info(connection) self._hostname = new_hostname self._connection = connection except DVIDException as ex: error_msg = "libdvid.DVIDException: {}".format( ex.message ) except ErrMsg as ex: error_msg = "libdvid.ErrMsg: {}".format( ex.message ) if error_msg: QMessageBox.critical(self, "Connection Error", error_msg) self._populate_node_list(None) else: self._connect_button.setEnabled(False) self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(True) enable_contents = self._repos_info is not None self._data_groupbox.setEnabled(enable_contents) self._node_groupbox.setEnabled(enable_contents) self._new_data_groupbox.setEnabled(enable_contents) self._populate_hostinfo_table() self._populate_repo_tree() @classmethod def _get_server_info(cls, connection): status, body, error_message = connection.make_request( "/server/info", ConnectionMethod.GET); assert status == httplib.OK, "Request for /server/info returned status {}".format( status ) assert error_message == "" server_info = json.loads(body) return server_info @classmethod def _get_repos_info(cls, connection): status, body, error_message = connection.make_request( "/repos/info", ConnectionMethod.GET) assert status == httplib.OK, "Request for /repos/info returned status {}".format( status ) assert error_message == "" repos_info = json.loads(body) # Discard uuids with 'null' content (I don't know why they sometimes exist...) repos_info = filter( lambda (uuid, repo_info): repo_info, repos_info.items() ) return collections.OrderedDict(sorted(repos_info)) def _populate_hostinfo_table(self): self._hostinfo_table.setVisible( self._server_info is not None ) if not self._server_info: return for column_index, fieldname in enumerate(SERVER_INFO_FIELDS): try: field = self._server_info[fieldname] except KeyError: field = "<UNDEFINED>" item = QTableWidgetItem(field) flags = item.flags() flags &= ~Qt.ItemIsSelectable flags &= ~Qt.ItemIsEditable item.setFlags( flags ) self._hostinfo_table.setItem(0, column_index, item) self._hostinfo_table.resizeColumnsToContents() self._hostinfo_table.horizontalHeader().setStretchLastSection(True) # Force refresh of last column. def _populate_repo_tree(self): """ Initialize the tree widget of repos and volumes. """ self._repo_treewidget.clear() if self._repos_info is None: return for repo_uuid, repo_info in sorted(self._repos_info.items()): if repo_info is None: continue repo_column_dict = collections.defaultdict(str) repo_column_dict["Alias"] = repo_info["Alias"] repo_column_dict["Details"] = "Created: " + repo_info["Created"] repo_column_dict["UUID"] = repo_uuid repo_column_values = [repo_column_dict[k] for k in TREEVIEW_COLUMNS] repo_item = QTreeWidgetItem( self._repo_treewidget, QStringList( repo_column_values ) ) repo_item.setData( 0, Qt.UserRole, (repo_uuid, "", "") ) for data_name, data_info in repo_info["DataInstances"].items(): data_instance_dict = collections.defaultdict(str) data_instance_dict["Alias"] = data_name typename = data_info["Base"]["TypeName"] data_instance_dict["TypeName"] = typename is_voxels = (typename in VOXEL_TYPENAMES) if is_voxels: start_coord = data_info["Extended"]["MinPoint"] if start_coord: start_coord = tuple(start_coord) stop_coord = data_info["Extended"]["MaxPoint"] if stop_coord: stop_coord = tuple(x+1 for x in stop_coord) if start_coord and stop_coord: shape = tuple(b - a for a,b in zip(start_coord, stop_coord)) else: shape = None data_instance_dict["Details"] = "Size={} | Start={} | Stop={}".format( shape, start_coord, stop_coord ) data_column_values = [data_instance_dict[k] for k in TREEVIEW_COLUMNS] data_item = QTreeWidgetItem( repo_item, data_column_values ) data_item.setData( 0, Qt.UserRole, (repo_uuid, data_name, typename) ) # If we're in specify_new mode, only the # repo parent items are selectable. # Also, non-volume items aren't selectable. if self._mode == 'specify_new' or typename not in self._selectable_types: flags = data_item.flags() flags &= ~Qt.ItemIsSelectable flags &= ~Qt.ItemIsEnabled data_item.setFlags( flags ) self._repo_treewidget.collapseAll() self._repo_treewidget.setSortingEnabled(True) if self._hostname in self._default_nodes: self._select_node_uuid(self._default_nodes[self._hostname]) self._repo_treewidget.resizeColumnToContents(0) self._repo_treewidget.setFocus() def _handle_data_selection(self): """ When the user clicks a new data item, respond by updating the node list. """ selected_items = self._repo_treewidget.selectedItems() if not selected_items: return None item = selected_items[0] item_data = item.data(0, Qt.UserRole).toPyObject() if not item_data: return repo_uuid, data_name, typename = item_data if self._current_repo != repo_uuid: self._populate_node_list(repo_uuid) self._update_status() def _populate_node_list(self, repo_uuid): """ Replace the contents of the node list widget to show all the nodes for the currently selected repo. """ self._node_listwidget.clear() if self._repos_info is None or repo_uuid is None: return # For now, we simply show the nodes in sorted order, without respect to dag order all_uuids = sorted( self._repos_info[repo_uuid]["DAG"]["Nodes"].keys() ) for node_uuid in all_uuids: node_item = QListWidgetItem( node_uuid, parent=self._node_listwidget ) node_item.setData( Qt.UserRole, node_uuid ) self._current_repo = repo_uuid # Select the last one by default. last_row = self._node_listwidget.count() - 1 last_item = self._node_listwidget.item( last_row ) self._node_listwidget.setCurrentItem( last_item ) self._update_status() def _get_selected_node(self): """ Return the currently selected node uuid. """ selected_items = self._node_listwidget.selectedItems() if not selected_items: return None selected_node_item = selected_items[0] node_item_data = selected_node_item.data(Qt.UserRole) return str( node_item_data.toString() ) def _get_selected_data(self): """ Return the repo, data name, and type of the currently selected data volume (or ROI). """ selected_items = self._repo_treewidget.selectedItems() if not selected_items: return None, None selected_data_item = selected_items[0] data_item_data = selected_data_item.data(0, Qt.UserRole).toPyObject() if selected_data_item: repo_uuid, data_name, typename = data_item_data else: repo_uuid = data_name = typename = None return repo_uuid, data_name, typename def _select_node_uuid(self, node_uuid): """ Locate the repo that owns this uuid, and select it in the GUI. If the uuid can't be found, do nothing. """ def select_repotree_item(repo_uuid): for row in range(self._repo_treewidget.topLevelItemCount()): repo_item = self._repo_treewidget.topLevelItem(row) if repo_uuid == repo_item.data(0, Qt.UserRole).toPyObject()[0]: self._repo_treewidget.setCurrentItem(repo_item) repo_item.setExpanded(True) self._repo_treewidget.scrollTo( self._repo_treewidget.selectedIndexes()[0], QTreeWidget.PositionAtCenter ) break def select_nodelist_item(node_uuid): for row in range(self._node_listwidget.count()): item = self._node_listwidget.item(row) if node_uuid == item.data(Qt.UserRole).toPyObject(): self._node_listwidget.setCurrentItem( item ) break for repo_uuid, repo_info in sorted(self._repos_info.items()): if node_uuid in repo_info["DAG"]["Nodes"].keys(): # Select the right repo parent item select_repotree_item(repo_uuid) # Select the right row in the node list # (The node list was automatically updated when the repo selection changed, above.) select_nodelist_item(node_uuid) break def _update_status(self): """ Update the path label to reflect the user's currently selected uuid and new volume name. """ hostname, repo_uuid, dataname, node_uuid, typename = self.get_selection() full_path = "http://{hostname}/api/node/{uuid}/{dataname}"\ "".format( hostname=self._hostname, uuid=node_uuid, dataname=dataname ) self._full_url_label.setText( full_path ) ok_button = self._buttonbox.button( QDialogButtonBox.Ok ) ok_button.setEnabled( dataname != "" )
class ExercicesTrigonometrie(Panel_API_graphique): titre = u"Trigonométrie" # Donner un titre a chaque module def __init__(self, *args, **kw): Panel_API_graphique.__init__(self, *args, **kw) self.canvas.fixe = True self.entrees = QVBoxLayout() self.entrees.addSpacing(30) self.panneau = QLabel('') self.entrees.addWidget(self.panneau) self.entrees.addStretch() self.felicitations = QLabel('') self.entrees.addWidget(self.felicitations) self.entrees.addSpacing(30) self.btn_niveau = QPushButton(u"Niveau suivant", self) self.btn_niveau.clicked.connect(self.niveau_suivant) self.entrees.addWidget(self.btn_niveau) self.entrees.addSpacing(50) self.sizer = QHBoxLayout() self.sizer.addWidget(self.canvas, 1) self.sizer.addLayout(self.entrees, 0.2) self.finaliser(contenu=self.sizer) self.reinitialiser() def reinitialiser(self): if param.debug: print(u'Module %s: réinitialisation...' % self.nom) self.score = 0 self.niveau = 0 self.erreurs = 0 self.niveau_suivant() def niveau_suivant(self, niveau=None): # On ferme toutes les feuilles ouvertes (inutile en principe), # et on en ouvre une nouvelle. self.fermer_feuilles() # Paramètres par défaut: self.canvas.fenetre = -8, 8, -10, 8 self.canvas.afficher_axes = True self.canvas.quadrillage_defaut() self.canvas.ratio = None self.canvas.repere = ('O', 'i', 'j') self.afficher_barre_outils(False) # Ne pas éditer les champs/textes avec [Entrée] self.canvas.editeur.actif = False # Ne pas éditer les objets par un clic droit self.canvas.edition_par_clic_droit = False # Et on change de niveau... if niveau in (None, False): # None ou False (False est renvoyé par Qt via QAbstractBouton.clicked) self.niveau += 1 else: self.niveau = niveau if param.debug: print("== Niveau %s ==" % self.niveau) getattr(self, 'niveau%s' % self.niveau)() self.btn_niveau.setEnabled(False) self.felicitations.setStyleSheet( """QLabel {background-color: white; padding: 5px; border-radius: 5px; color:white;}""") self.update_panneau() n = niveau_suivant # ------------------------------------------------------ # Niveaux 1 à 7 : lecture graphique d'équation de droite # ------------------------------------------------------ def cercle_trigo(self): u"""Construction d'un cercle trigonométrique. """ obj = self.feuille_actuelle.objets obj['O'] = O = Point(0, 0, fixe=True) obj['I'] = I = Point(1, 0, fixe=True) obj['c'] = c = Cercle(O, I) for i in range(1, 8): a = i*pi/4 M = Point(cos(a), sin(a), fixe=True) for i in range(1, 12): if i != 6: a = i*pi/6 M = Point(cos(a), sin(a), fixe=True) self.feuille_actuelle.objets['B'] = B # Ne pas afficher l'équation ! d = Droite(A, B, afficher_info=False) self.feuille_actuelle.objets['d'] = d if xA == xB: reponse = 'x=' + str(xA) else: reponse = ('y=%s*x+%s' % self.eq_reduite(pointA, pointB)) xmin, xmax, ymin, ymax = self.canvas.fenetre print 'Fenetre::', self.canvas.fenetre, '--', xmin, ymin champ = Champ('', xmin, ymin, fond=True, couleur_fond='#ffffb5', prefixe=(ur"Dans le repère $(O;\,\vec\imath,\,\vec\jmath)$, " u"la droite $(AB)$ a pour équation "), alignement_horizontal='left', alignement_vertical='bottom', attendu=reponse) print 'xy::', champ.xy champ.valider = self.valider_eq champ.evt_valider = self.compter_points self.feuille_actuelle.objets['champ1'] = champ def niveau1(self): yA = self.relatif(7) if yA > 0: yB = yA - randint(1, 3) else: yB = yA + randint(1, 3) self.exercice_lire_equation_AB((0, yA), (1, yB)) def niveau2(self): yA = self.relatif(7) if yA > 0: yB = yA - choice([1, 3, 5]) else: yB = yA + choice([1, 3, 5]) self.exercice_lire_equation_AB((0, yA), (-2, yB)) def niveau3(self): while True: yA = self.relatif(7) yB = self.relatif(7) if abs(yB - yA) > 5: break while True: xB = self.relatif(7) if abs(xB) > 5: break self.exercice_lire_equation_AB((0, yA), (xB, yB)) def niveau4(self): u"""Droite horizontale.""" yA = self.relatif(7) while True: xA = self.relatif(7) xB = self.relatif(7) if abs(xB - xA) > 5: break self.exercice_lire_equation_AB((xA, yA), (xB, yA)) def niveau5(self): u"""Droite verticale.""" xA = self.relatif(7) while True: yA = self.relatif(7) yB = self.relatif(7) if abs(yB - yA) > 5: break self.exercice_lire_equation_AB((xA, yA), (xA, yB)) def niveau6(self, n=7): """Droite oblique ne coupant pas l'axe des ordonnées sur une graduation ; il faut donc calculer (ou deviner) l'ordonnée à l'origine.""" for i in xrange(1000): while True: xA = self.relatif(n) xB = self.relatif(n) if abs(xB - xA) > 5: break while True: yA = self.relatif(7) yB = self.relatif(7) if abs(yB - yA) > 5: break # On calcule l'ordonnée à l'origine (sous forme de fraction sympy). a, b = self.eq_reduite((xA, yA), (xB, yB)) print b if b.q not in (1, 2): # Le dénominateur de l'ordonnée à l'origine ne doit pas être 1 ou 2. break self.exercice_lire_equation_AB((xA, yA), (xB, yB)) def niveau7(self): self.canvas.ratio = 4 self.canvas.quadrillages = (((.25, 1), ':', 0.5, 'k'),) self.canvas.fenetre = -4.5, 4.5, -10, 8 print self.canvas.fenetre self.niveau6(n=4) self.feuille_actuelle.objets['champ1'].y = -9 def valider_eq(self, reponse, attendu): reponse = traduire_formule(reponse) assert attendu.count('=') == 1 if reponse.count('=') != 1: return False g, d = attendu.split('=') attendu = '%s-(%s)' %(g, d) g, d = reponse.split('=') reponse = '%s-(%s)' %(g, d) reponse = reponse.replace(',', '.') quotient = '(%s)/(%s)' % (reponse, attendu) try: # Les deux équations doivent être « proportionelles ». return not simplify(S(quotient)).free_symbols except (SympifyError, ValueError): pass except Exception: print_error() return False def eq_reduite(self, A, B): u"""Équation réduite exacte de la droite (AB). La droite ne doit pas être verticale.""" xA, yA = A xB, yB = B assert xA != xB, "Droite verticale !" a = S(yB - yA)/(xB - xA) b = yA - a*xA return a, b # -------------------------------------------- # Niveau 8 : Résoudre graphiquement un système # -------------------------------------------- def niveau8(self): u"""Résolution graphique de système. Construire deux droites d'équations données. Lire les coordonnées du point d'intersection.""" self.canvas.fenetre = -8, 8, -10, 11 self.canvas.afficher_axes = True self.canvas.quadrillage_defaut() self.afficher_barre_outils(True) self.canvas.grille_aimantee = True ##self.canvas.editeur.actif = True # Point d'intersection C = self.couple() # On génère deux points A, et B, appartenant respectivement # aux droites d1 et d2. # Contraintes : # - les points A, B, C ne doivent pas être alignés ; # - les droites (AC) et (BC) ne doivent être ni horizontales, # ni verticales. while True: A = self.couple() B = self.couple() # A, B, C non alignés if det(vect(A, C), vect(B, C)) == 0: continue # (AC) et (BC) ni verticales, ni horizontales if all((A[0] - C[0], A[1] - C[1], B[0] - C[0], B[1] - C[1])): break if param.debug: print 'A,B,C:', A, B, C # on génère les deux équations de droite x = S('x') ##def eq_latex(pt1, pt2): ##a, b = self.eq_reduite(pt1, pt2) ##a = str(a) ##if a in ('1', '-1'): ##a = a[:-1] ##b = str(b) ##return convertir_en_latex('y=%s*x+%s' % (a, b)) def format(eq): eq = eq.replace('=1*x', '=x').replace('=-1*x', '=-x').replace('+0', '') eq = convertir_en_latex(eq) return eq a, b = self.eq_reduite(A, C) eq1 = format('y=%s*x+%s' % (a, b)) c, d = self.eq_reduite(B, C) eq2 = format('y=%s*x+%s' % (c, d)) xmin, xmax, ymin, ymax = self.canvas.fenetre txt = Texte((u"On note $d_1$ la droite d'équation %s, " u"et $d_2$ la droite d'équation %s.\n" u"Construire les droites $d_1$ puis $d_2$ dans le repère ci-dessous.") % (eq1, eq2), xmin, ymax, fond=True, couleur_fond='#ffffb5', fixe=True, alignement_horizontal='left', alignement_vertical='top') self.feuille_actuelle.objets['txt1'] = txt champ = Champ('', xmin, ymin, prefixe=u"Le couple solution du système est (", alignement_vertical='bottom', alignement_horizontal='left', attendu=str(C), fond=True, couleur_fond='#ffffb5', suffixe=')', ) self.feuille_actuelle.objets['champ1'] = champ champ.valider = self.valider_couple champ.evt_valider = self.compter_points ch1 = Champ('', visible=False, attendu='ok') self.feuille_actuelle.objets['champ_cache_d1'] = ch1 ch1.evt_valider = self.compter_points ch2 = Champ('', visible=False, attendu='ok') ch2.evt_valider = self.compter_points self.feuille_actuelle.objets['champ_cache_d2'] = ch2 self.feuille_actuelle.lier(partial(self.verifier_feuille, eq1=(a, b), eq2=(c, d))) def verifier_feuille(self, eq1, eq2): print eq1, eq2 for nom, eq in (('d1', eq1), ('d2', eq2)): if nom in self.feuille_actuelle.objets.noms: d = self.feuille_actuelle.objets[nom] d.label(mode='nom') champ = self.feuille_actuelle.objets['champ_cache_' + nom] M, N = d M = (int(M.x), int(M.y)) N = (int(N.x), int(N.y)) if self.eq_reduite(M, N) == eq: d.style(couleur='g') champ.texte = 'ok' msg = 'La droite %s est correcte.' % nom if nom == 'd1': msg += ' Construisez maintenant d2.' else: print self.eq_reduite(*d), eq d.style(couleur='r') # On peut mettre n'importe quoi différent de ok dans # champ, l'idée étant que si la droite est fausse mais # n'a pas changé, on ne perde pas de point, et par # contre on perde des points en cas de changement si # c'est toujours faux. champ.texte = str(d.equation) msg = "Attention, la droite %s est fausse." % nom QToolTip.showText(QCursor.pos(), msg) self.canvas.message(msg, temporaire=False) def valider_couple(self, reponse, attendu): reponse = reponse.replace(';', ',') try: if S(reponse) == S(attendu): return True except Exception: print_error() return False def ax_b(self): u"Générer une expression sympy de la forme ax+b, avec a, b dans Z." return self.relatif()*S('x') + self.relatif() def update_panneau(self): self.panneau.setStyleSheet( """QLabel { padding: 10px; border-width: 2px; border-style:solid; border-radius: 5px; border-color:%s; background-color: %s }""" %(QColor(30, 144, 255).name(), QColor(176, 226, 255).name()) ) self.panneau.setText((u"<p><b><i>Niveau :</i> %s</b></p>" % self.niveau) + (u"<p><b><i>Points :</i> %s</b></p>" % self.score) + (u"<p><i>Erreurs :</i> %s</p>" % self.erreurs)) champs = self.feuille_actuelle.objets.lister(type=Champ) if champs and all(obj.correct for obj in champs): if hasattr(self, 'niveau' + str(self.niveau + 1)): self.btn_niveau.setEnabled(True) self.btn_niveau.setFocus(True) self.felicitations.setText(u'<p><b>Félicitations !</b></p>' + u'<p>Passer au niveau %s</p>' %(self.niveau + 1)) self.felicitations.setStyleSheet( """QLabel {background-color: %s; padding: 5px; border-radius: 5px; color:white;}""" %QColor(255, 153, 0).name()) else: self.felicitations.setText(u'<p><b>Félicitations !</b></p>' + u'<p>Dernier niveau terminé !</p>') self.felicitations.setStyleSheet( """QLabel {background-color: %s; padding: 5px; border-radius: 5px; color:white;}""" %QColor(102, 205, 0).name()) ##def _sauvegarder(self, fgeo, feuille = None): ##Panel_API_graphique._sauvegarder(self, fgeo, feuille) ##fgeo.contenu[u"niveau"] = [str(self.niveau)] ##fgeo.contenu[u"expression"] = [self.raw_expression] ##fgeo.contenu[u"score"] = [str(self.score)] ##fgeo.contenu[u"erreurs"] = [str(self.erreurs)] ## def _ouvrir(self, fgeo): pass ### Il ne doit y avoir qu'une seule feuille ouverte à la fois. ### XXX: intégrer cette fonctionnalité directement au Panel. ##self.fermer_feuilles() ##Panel_API_graphique._ouvrir(self, fgeo) ##if fgeo.contenu.has_key(u"expression"): ##self.generer_expression(expr=fgeo.contenu[u"expression"][0]) ##self.dessiner_tableau() ##if fgeo.contenu.has_key(u"niveau"): ##self.niveau = int(fgeo.contenu[u"niveau"][0]) ##if fgeo.contenu.has_key(u"score"): ##self.score = int(fgeo.contenu[u"score"][0]) ##if fgeo.contenu.has_key(u"erreurs"): ##self.erreurs = int(fgeo.contenu[u"erreurs"][0]) ##self.update_panneau() ##def _affiche(self): ##self.dessiner_tableau() # -------------------------------- # Génération de nombres aléatoires # -------------------------------- @staticmethod def signe(): return 2*randint(0, 1) - 1 def naturel(self, n=15): u'''Retourne un entier entre 2 et `n`.''' return randint(2, n) def relatif(self, n=15): u'''Retourne un entier entre -`n` et -2, ou entre 2 et `n`.''' # signe: 1 ou -1 signe = 2*randint(0, 1) - 1 return self.signe()*self.naturel(n) def rationnel(self, n=7): u'''Retourne un quotient d'entiers.''' while True: p = self.naturel(n) q = self.naturel(n) if p%q: break return self.signe()*S(p)/S(q) def couple(self, m=7, n=7): u"""Retourne un couple d'entiers relatifs.""" return self.relatif(m), self.relatif(n) def autocompleter(self): ##if self.btn_niveau.isEnabled(): ##self.niveau_suivant() self.btn_niveau.click() for t in self.feuille_actuelle.objets.lister(type=Champ): t.texte = t.style('attendu', color='g') ##self.parent.parent.ligne_commande.setFocus() a = property(autocompleter) def compter_points(self, **kw): if 'correct' in kw and 'correct_old' in kw and 'champ' in kw: if kw['correct']: if not kw['correct_old']: if not kw['champ'].style('choix'): # C'est plus dur s'il n'y a pas de choix proposé self.score += 1 self.score += 1 else: self.score -= 1 self.erreurs += 1 if all(obj.correct for obj in self.feuille_actuelle.objets.lister(type=Champ)): self.score += 10*(self.niveau + 1) self.update_panneau()
class ContentsBrowser(QDialog): """ Displays the contents of a DVID server, listing all datasets and the volumes/nodes within each dataset. The user's selected dataset, volume, and node can be accessed via the `get_selection()` method. If the dialog is constructed with mode='specify_new', then the user is asked to provide a new data name, and choose the dataset and node to which it will belong. **TODO:** * Show more details in dataset list (e.g. shape, axes, pixel type) * Show more details in node list (e.g. date modified, parents, children) * Gray-out nodes that aren't "open" for adding new volumes """ def __init__(self, suggested_hostnames, mode='select_existing', parent=None): """ Constructor. suggested_hostnames: A list of hostnames to suggest to the user, e.g. ["localhost:8000"] mode: Either 'select_existing' or 'specify_new' parent: The parent widget. """ super( ContentsBrowser, self ).__init__(parent) self._suggested_hostnames = suggested_hostnames self._mode = mode self._current_dset = None self._repos_info = None self._hostname = None # Create the UI self._init_layout() VolumeSelection = collections.namedtuple( "VolumeSelection", "hostname dataset_uuid data_name node_uuid" ) def get_selection(self): """ Get the user's current (or final) selection. Returns a VolumeSelection tuple. """ node_uuid = self._get_selected_node() dset_uuid, data_name = self._get_selected_data() if self._mode == "specify_new": data_name = str( self._new_data_edit.text() ) return ContentsBrowser.VolumeSelection(self._hostname, dset_uuid, data_name, node_uuid) def _init_layout(self): """ Create the GUI widgets (but leave them empty). """ hostname_combobox = QComboBox(parent=self) self._hostname_combobox = hostname_combobox hostname_combobox.setEditable(True) hostname_combobox.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Maximum ) hostname_combobox.installEventFilter(self) for hostname in self._suggested_hostnames: hostname_combobox.addItem( hostname ) self._connect_button = QPushButton("Connect", parent=self, clicked=self._handle_new_hostname) hostname_layout = QHBoxLayout() hostname_layout.addWidget( hostname_combobox ) hostname_layout.addWidget( self._connect_button ) hostname_groupbox = QGroupBox("DVID Host", parent=self) hostname_groupbox.setLayout( hostname_layout ) hostname_groupbox.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Maximum ) data_treewidget = QTreeWidget(parent=self) data_treewidget.setHeaderLabels( ["Data"] ) # TODO: Add type, shape, axes, etc. data_treewidget.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred ) data_treewidget.itemSelectionChanged.connect( self._handle_data_selection ) data_layout = QVBoxLayout() data_layout.addWidget( data_treewidget ) data_groupbox = QGroupBox("Data Volumes", parent=self) data_groupbox.setLayout( data_layout ) node_listwidget = QListWidget(parent=self) node_listwidget.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred ) node_listwidget.itemSelectionChanged.connect( self._update_display ) node_layout = QVBoxLayout() node_layout.addWidget( node_listwidget ) node_groupbox = QGroupBox("Nodes", parent=self) node_groupbox.setLayout( node_layout ) new_data_edit = QLineEdit(parent=self) new_data_edit.textEdited.connect( self._update_display ) full_url_label = QLabel(parent=self) full_url_label.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Maximum ) text_flags = full_url_label.textInteractionFlags() full_url_label.setTextInteractionFlags( text_flags | Qt.TextSelectableByMouse ) new_data_layout = QVBoxLayout() new_data_layout.addWidget( new_data_edit ) new_data_groupbox = QGroupBox("New Data Volume", parent=self) new_data_groupbox.setLayout( new_data_layout ) new_data_groupbox.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Maximum ) buttonbox = QDialogButtonBox( Qt.Horizontal, parent=self ) buttonbox.setStandardButtons( QDialogButtonBox.Ok | QDialogButtonBox.Cancel ) buttonbox.accepted.connect( self.accept ) buttonbox.rejected.connect( self.reject ) buttonbox.button(QDialogButtonBox.Ok).setEnabled(False) layout = QVBoxLayout() layout.addWidget( hostname_groupbox ) layout.addWidget( data_groupbox ) layout.addWidget( node_groupbox ) if self._mode == "specify_new": layout.addWidget( new_data_groupbox ) else: new_data_groupbox.hide() layout.addWidget( full_url_label ) layout.addWidget( buttonbox ) # Stretch factors layout.setStretchFactor(data_groupbox, 3) layout.setStretchFactor(node_groupbox, 1) self.setLayout(layout) self.setWindowTitle( "Select DVID Volume" ) # Initially disabled data_groupbox.setEnabled(False) node_groupbox.setEnabled(False) new_data_groupbox.setEnabled(False) # Save instance members self._data_groupbox = data_groupbox self._node_groupbox = node_groupbox self._new_data_groupbox = new_data_groupbox self._data_treewidget = data_treewidget self._node_listwidget = node_listwidget self._new_data_edit = new_data_edit self._full_url_label = full_url_label self._buttonbox = buttonbox def sizeHint(self): return QSize(700, 500) def eventFilter(self, watched, event): if watched == self._hostname_combobox \ and event.type() == QEvent.KeyPress \ and ( event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter): self._connect_button.click() return True return False def showEvent(self, event): """ Raise the window when it is shown. For some reason, that doesn't happen automatically if this widget is also the main window. """ super(ContentsBrowser, self).showEvent(event) self.raise_() def _handle_new_hostname(self): new_hostname = str( self._hostname_combobox.currentText() ) if '://' in new_hostname: new_hostname = new_hostname.split('://')[1] error_msg = None self._repos_info = None self._current_dset = None self._hostname = None try: # Query the server connection = httplib.HTTPConnection( new_hostname ) self._repos_info = pydvid.general.get_repos_info( connection ) self._hostname = new_hostname self._connection = connection except socket.error as ex: error_msg = "Socket Error: {} (Error {})".format( ex.args[1], ex.args[0] ) except httplib.HTTPException as ex: error_msg = "HTTP Error: {}".format( ex.args[0] ) if error_msg: QMessageBox.critical(self, "Connection Error", error_msg) self._populate_datasets_tree() self._populate_node_list(-1) else: self._connect_button.setEnabled(False) self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(True) enable_contents = self._repos_info is not None self._data_groupbox.setEnabled(enable_contents) self._node_groupbox.setEnabled(enable_contents) self._new_data_groupbox.setEnabled(enable_contents) self._populate_datasets_tree() def _populate_datasets_tree(self): """ Initialize the tree widget of datasets and volumes. """ self._data_treewidget.clear() if self._repos_info is None: return for dset_uuid, dset_info in sorted(self._repos_info.items()): dset_item = QTreeWidgetItem( self._data_treewidget, QStringList( dset_uuid ) ) dset_item.setData( 0, Qt.UserRole, (dset_uuid, "") ) for data_name in dset_info["DataInstances"].keys(): data_item = QTreeWidgetItem( dset_item, QStringList( data_name ) ) data_item.setData( 0, Qt.UserRole, (dset_uuid, data_name) ) if self._mode == 'specify_new': # If we're in specify_new mode, only the dataset parent items are selectable. flags = data_item.flags() flags &= ~Qt.ItemIsSelectable flags &= ~Qt.ItemIsEnabled data_item.setFlags( flags ) # Expand everything self._data_treewidget.expandAll() # Select the first item by default. if self._mode == "select_existing": first_item = self._data_treewidget.topLevelItem(0).child(0) else: first_item = self._data_treewidget.topLevelItem(0) self._data_treewidget.setCurrentItem( first_item, 0 ) def _handle_data_selection(self): """ When the user clicks a new data item, respond by updating the node list. """ selected_items = self._data_treewidget.selectedItems() if not selected_items: return None item = selected_items[0] item_data = item.data(0, Qt.UserRole).toPyObject() if not item_data: return dset_uuid, data_name = item_data if self._current_dset != dset_uuid: self._populate_node_list(dset_uuid) self._update_display() def _populate_node_list(self, dataset_uuid): """ Replace the contents of the node list widget to show all the nodes for the currently selected dataset. """ self._node_listwidget.clear() if self._repos_info is None: return # For now, we simply show the nodes in sorted order, without respect to dag order all_uuids = sorted( self._repos_info[dataset_uuid]["DAG"]["Nodes"].keys() ) for node_uuid in all_uuids: node_item = QListWidgetItem( node_uuid, parent=self._node_listwidget ) node_item.setData( Qt.UserRole, node_uuid ) self._current_dset = dataset_uuid # Select the last one by default. last_row = self._node_listwidget.count() - 1 last_item = self._node_listwidget.item( last_row ) self._node_listwidget.setCurrentItem( last_item ) self._update_display() def _get_selected_node(self): selected_items = self._node_listwidget.selectedItems() if not selected_items: return None selected_node_item = selected_items[0] node_item_data = selected_node_item.data(Qt.UserRole) return str( node_item_data.toString() ) def _get_selected_data(self): selected_items = self._data_treewidget.selectedItems() if not selected_items: return None, None selected_data_item = selected_items[0] data_item_data = selected_data_item.data(0, Qt.UserRole).toPyObject() if selected_data_item: dset_uuid, data_name = data_item_data else: dset_uuid = data_name = None return dset_uuid, data_name def _update_display(self): """ Update the path label to reflect the user's currently selected uuid and new volume name. """ hostname, dset_uuid, dataname, node_uuid = self.get_selection() full_path = "http://{hostname}/api/node/{uuid}/{dataname}"\ "".format( hostname=self._hostname, uuid=node_uuid, dataname=dataname ) self._full_url_label.setText( full_path ) ok_button = self._buttonbox.button( QDialogButtonBox.Ok ) ok_button.setEnabled( dataname != "" )
class NotesMainWindow(QMainWindow): """The view class""" def __init__(self, parent=None): super().__init__(parent) # Load saved application settings self.recentNotes = [] self.numberOfTabs = 0 # keep track of the total number of tabs in the window self.currentPage = None self.__mediator = MITM.Mediator.Mediator( ) # the mediator object. intermediary btwn view and model # create a timer timer = QTimer(self) # dialogs: saved to avoid eating memory by constant cre- and re- creation self.findDialog = None self.renameDialog = None self.helpAboutDialog = HelpAboutDialog(self) self.helpHelpDialog = HelpHelpDialog(self) self.recentFilesListWidget = QListWidget # create menus first self.fileMenu = self.menuBar().addMenu("File") self.recentNotesMenu = self.fileMenu.addMenu("Open Recent") self.editMenu = self.menuBar().addMenu("Edit") self.syncMenu = self.menuBar().addMenu("Sync") self.helpMenu = self.menuBar().addMenu("Help") # region Actions # create the menu actions # first the file menu actions fileNewAct = self.createAction(self, "&New", shortcut=QKeySequence.New, tooltip="Create a new note", enabled=True, slot=self.onNewClick) fileOpenRecentNotesAct = self.createAction(self, "&Open Recent Note", tooltip="open recent notes", enabled=True, slot=self.onOpenRecentNotes) fileQuitAct = self.createAction(self, "&Exit", shortcut=QKeySequence.Quit, tooltip="Exit the application", enabled=True, slot=self.close) # recent files menu actions self.recentFilesMenuAct = self.createAction(self.recentNotesMenu, "File 1") # edit menu actions editInsertAct = self.createAction(self, "Ins&ert...", shortcut="Ctrl+I", tooltip="Insert a media file", enabled=True) editPreferencesAct = self.createAction( self, "&Preferences", shortcut="Ctrl+Shift+P", tooltip="Set application preferences", enabled=True) editFindAct = self.createAction(self, "&Find", shortcut=QKeySequence.Find, tooltip="Find a text string", slot=self.onFind) editUndoAct = self.createAction( self, "&Undo", shortcut=QKeySequence.Undo, tooltip="Roll back changes to document", slot=self.onUndo) editRedoAct = self.createAction(self, "&Redo", shortcut=QKeySequence.Redo, tooltip="Repeat the last action", slot=self.onRedo) editRenameAct = self.createAction(self, "&Rename...", shortcut="Ctrl+Shift+R", tooltip="Rename current note", slot=self.onRenameNote) # sync menu actions synCloudAcctAct = self.createAction(self, "Cloud &Account", shortcut="Alt+A", tooltip="Sync with cloud accounts", enabled=True) # help menu actions helpHelpAct = self.createAction(self, "Help", shortcut=QKeySequence.HelpContents, tooltip="Display help", enabled=True, slot=self.onHelpHelp) helpAboutAct = self.createAction(self, "A&bout", shortcut="Ctrl+Shift+B", tooltip="About application", enabled=True, slot=self.onHelpAbout) boldTextAction = self.createAction(self, "B", shortcut="Ctrl+B", tooltip="Bold text") italicsTextAction = self.createAction(self, "<i>I</i>", shortcut="Ctrl+I", tooltip="Italics text") # endregion # add actions to corresponding menu self.addActions_( self.fileMenu, (fileNewAct, fileOpenRecentNotesAct, fileQuitAct)) # to file menu self.addActions_(self.editMenu, (editRenameAct, editUndoAct, editRedoAct, editFindAct, editInsertAct, editPreferencesAct)) # to edit menu self.addActions_(self.helpMenu, (helpHelpAct, helpAboutAct)) # to help menu self.addActions_(self.recentNotesMenu, (self.recentFilesMenuAct, )) # create tool bar and add relevant actions allToolBar = self.addToolBar( "All Tools" ) # tool bar that contains all tool;not separated into file/edit/* self.addActions_(allToolBar, (fileNewAct, synCloudAcctAct, editInsertAct, editFindAct, editUndoAct, editRedoAct)) fontDetailsToolBar = self.addToolBar("Font Details") self.fontTypeComboBox = QFontComboBox( fontDetailsToolBar) # get font list present on the user system self.fontSizeSpinBox = QSpinBox(fontDetailsToolBar) # size of the font self.fontSizeSpinBox.setMinimum(7) self.fontSizeSpinBox.setValue(12) self.textBoldButton = QPushButton("B") self.addNewNoteButton = QPushButton("+") self.textItalicsButton = QPushButton("I") maskLabel = QLabel( fontDetailsToolBar) # TODO: experimental, to be removed pixmap = QPixmap("Asset/guit.png") # TODO: experimental, remove maskLabel.setPixmap(pixmap.mask()) # TODO: experimental, remove self.addWidgetToToolBar(fontDetailsToolBar, (self.fontTypeComboBox, self.fontSizeSpinBox, self.textBoldButton, self.textItalicsButton)) # self.addActions_(fontDetailsToolBar, (boldTextAction, italicsTextAction)) # create a QTabWidget self.tabWidget = QTabWidget(self) self.tabWidget.setMovable(True) self.tabWidget.setTabsClosable(True) self.tabWidget.setTabPosition(QTabWidget.South) # self.setTabPosition(Qt.BottomDockWidgetArea, QTabWidget.South) # TODO: not working yet. Handled self.tabWidgetTabBar = self.tabWidget.tabBar() self.tabWidget.setCornerWidget(self.addNewNoteButton, Qt.TopRightCorner) # create pages # TODO: let an event handler handle this job: HANDLED self.onWindowStartOpenNote() # self.page1 = View.TabbedPage.TabbedPage(self, Note("First", "/path")) # # add the pages to the tab Widget # self.tabWidget.addTab(self.page1, "Note 1 ") # self.currentPage = self.tabWidget.currentWidget() self.setCurrentPage() # print("count() returned: ", self.tabWidget.count()) # create "+" bar: TODO: find a better way to create a plus tab: DONE # self.plusTabIndex = self.tabWidgetTabBar.insertTab(self.tabWidget.count() + 1, # "+") # this tab bears the "+" sign that indicates 'create new tab' # self.plusTabWidget = self.tabWidget.widget(self.plusTabIndex) # get a reference to the plus tab widget # create Dock widget that holds find dialog # self.dockWidget = QDockWidget(self) # self.dockWidget.setAllowedAreas(Qt.TopDockWidgetArea) # self.dockWidget.setFeatures(QDockWidget.DockWidgetClosable|QDockWidget.DockWidgetMovable) # self.dockWidget.setWidget(FindDialog.FindDialog(self.currentPage, self)) # self.dockWidget.hide() # do window namings and other stuffs self.statusbar = QStatusBar(self) self.statusbar.setSizeGripEnabled(False) self.setStatusBar(self.statusbar) self.setCentralWidget(self.tabWidget) self.setWindowTitle("Notes") # region Signal And Slot Bindings self.tabWidget.currentChanged.connect(self.setCurrentPage) # self.currentPage.firstTime.connect(self.openPageOnFirstNavigation) self.tabWidget.currentChanged.connect( self.notifyMediatorOfCurrentPageChange) # fileNewAct.triggered.connect(self.onNewClick) # fileQuitAct.triggered.connect(self.close) self.tabWidget.tabCloseRequested.connect( self.confirmTabCloseAndCloseTab) # self.tabWidget.tabCloseRequested.connect(self.holla) self.fontTypeComboBox.currentFontChanged.connect(self.changeFont) self.fontSizeSpinBox.valueChanged.connect(self.changeFontSize) self.textBoldButton.clicked.connect(self.toggleTextBold) self.textItalicsButton.clicked.connect(self.toggleTextItalics) timer.timeout.connect(self.onSaveClick) timer.timeout.connect(self.timed) self.addNewNoteButton.clicked.connect(self.onNewClick) self.currentPage.cursorPositionChanged.connect( self.reportCurrentCursor) self.recentNotesMenu.aboutToShow.connect(self.updateRecentNotesMenu) # editUndoAct.triggered.connect(self.currentPage.undo) # editRedoAct.triggered.connect(self.currentPage.redo) # endregion self.readSettings() # QTimer.singleShot(5000, self.onSaveClick) # Start the timer timer.start(1500000) # Application settings # region Methods def updateRecentNotesMenu(self): """Repopulate the recent files menu""" self.recentNotesMenu.clear() for note in self.recentNotes: if os.path.exists(self.mediator.returnNotePath(note)): noteName, ext = os.path.basename(note).split(".") act = self.createAction( self.recentNotesMenu, noteName, slot=self.loadNotes ) #TODO: incomplete update loadNotes() to receive the noteName self.addActions_(self.recentNotesMenu, (act, )) def onOpenRecentNotes(self): """""" def reportCurrentCursor(self): print("###### cursor position has changed now #####") # TODO: remove print("current font is: ", self.currentPage.currentFont().family()) self.fontTypeComboBox.setCurrentFont(self.currentPage.currentFont()) self.fontSizeSpinBox.setValue( self.currentPage.currentFont().pointSize()) def onRecentNotes(self): self.recentFilesListWidget.show() def toggleTextItalics(self): self.currentPage.setFontItalic(not self.currentPage.fontItalic()) self.currentPage.setFocus() # if not self.textItalicsButton.isChecked(): # self.textItalicsButton.setChecked(True) # self.currentPage.setFontItalic(True) # self.currentPage.setFocus() # return # self.textItalicsButton.setChecked(False) # self.currentPage.setFontItalic(False) # self.currentPage.setFontItalic(checked) # self.currentPage.setFontItalic(not self.currentPage.fontItalic) def keyPressEvent(self, event: QKeyEvent): if event.modifiers() and Qt.ControlModifier: handled = False if event.key() == Qt.Key_B: self.toggleTextBold() # bolden text handled = True elif event.key() == Qt.Key_I: self.toggleTextItalics() # italicise text handled = True print("Ctrl+I") if handled: event.accept() return else: QTextEdit.keyPressEvent(self.currentPage, event) def toggleTextBold(self): self.currentPage.setFontWeight( QFont.Normal if self.currentPage.fontWeight() > QFont.Normal else QFont.Bold) # if not self.textBoldButton.isChecked(): # # self.textBoldButton.setChecked(True) # self.currentPage.setFontWeight(QFont.Bold) # self.currentPage.setFocus() # return # # self.textBoldButton.setChecked(False) # self.currentPage.setFontWeight(QFont.Normal) self.currentPage.setFocus() # self.currentPage.setFontWeight(QFont.Bold) if checked else \ # self.currentPage.setFontWeight(QFont.Normal) # self.currentPage.setFocus() def changeFontSize(self, value: float): self.currentPage.setFontPointSize(value) self.currentPage.setFocus() def changeFont(self, font: QFont): self.currentPage.setCurrentFont(font) self.currentPage.setFocus() def addWidgetToToolBar(self, toolBar: QToolBar, widgets: tuple): for widget in widgets: toolBar.addWidget(widget) def holla(self, index): """Debugging Method. To be removed in release code""" print("<<<<**** Hey! holla! Removing: .... ", index) def timed(self): """Debugging Method. To be removed in release code""" print("\n$$$$> Called by timer") # region Methods def onWindowStartOpenNote(self): for note_file in self.findNotesInCurrentDirectory( applicationDirectory): # find all the notes in the app dir self.createNoteObjectFromNoteFile( note_file) # create note objs from the files self.createAndLoadNotes() def createNoteObjectFromNoteFile(self, note_file: str): self.mediator.createNote(notePath=note_file) def createAndLoadNotes( self): # Trent Harmon: When a man loves a woman....song request if len(self.mediator.notesCollection) > 0: self.loadNotes() else: # couldn't find a note obj self.createOnlyOneNote() def loadNotes(self): """Creates a TabbedPage for each note found in notes collection Without reading the contents of the associated note file found In note path""" note = self.sender().getData() if self.sender() is QAction else None if note: pageIndex = self.createNotePage(note) self.tabWidget.setCurrentIndex(pageIndex) for note in self.mediator.NotesCollection: self.createNotePage(note) # self.mediator.setCurrentlyDisplayedNote(note) def createNotePage(self, note): page = View.TabbedPage.TabbedPage(self, note) pageIndex = self.tabWidget.addTab(page, note.NoteName) page.page_number = pageIndex return pageIndex def createOnlyOneNote(self): page1 = View.TabbedPage.TabbedPage(self, Note("First", "/path")) # add the pages to the tab Widget self.tabWidget.addTab(page1, "Note 1 ") def openPageOnFirstNavigation(self): """Reads the content of the file on first nav. Implements something like lazy loading Implementation is whack. Implementation Path: onWindowStart -> createNoteObjectFromNoteFile -> createAndLoadNotes -> loadNotes -> openPageOnFirstNavigation TODO: make a better lazy loading implementatiion""" # TODO: make a better lazy loading implementatiion print("\n<<<<<I also work\n") self.mediator.setCurrentlyDisplayedNote(self.currentPage.getNote()) try: contents = self.mediator.openCurrentNote() self.currentPage.setHtml(contents) self.currentPage.moveCursor( QTextCursor.End, QTextCursor.MoveAnchor ) # move the cursor to the end of the document self.currentPageIsNowDirty(False) except IOError as e: logging.error("Note is not on filesystem yet: {}".format(e)) def currentPageIsNowDirty(self, bool_: bool = False): self.currentPage.dirty = False @property def mediator(self): return self.__mediator @mediator.setter def mediator(self, newMediator): if isinstance(newMediator, MITM.Mediator.Mediator): self.__mediator = newMediator else: raise TypeError("newMediator must be of type Mediator") def writeSettings(self) -> None: settings = QSettings() settings.setValue("mainWindow/pos", self.pos()) settings.setValue("mainWindow/size", self.size()) settings.setValue("mainWindow/state", self.windowState()) openedNotes = [note.NotePath for note in self.mediator.NotesCollection] settings.setValue("list of opened notes", openedNotes) fontAttrList = self.fontTypeComboBox.currentFont().toString() print("/////////>>>> ", fontAttrList) settings.setValue("mainWindow/font", fontAttrList) settings.setValue("mainWindow/fontSize", self.fontSizeSpinBox.value()) def readSettings(self): settings = QSettings() self.resize(settings.value("mainWindow/size", QSize(800, 600))) pos = settings.value("mainWindow/pos", QPoint(0, 0)) listOfOpenedNotes = settings.value("list of opened notes") print("Opened notes:", listOfOpenedNotes) fontDetails = settings.value("mainWindow/font", "Architext, -1, -1, False") fontSize = settings.value("mainWindow/fontSize", 12) family, _, _, fontWeight, italics, *_ = fontDetails.split(",") font = QFont(family, int(fontSize), int(fontWeight), bool(italics)) self.fontTypeComboBox.setCurrentFont(font) self.fontSizeSpinBox.setValue(int(fontSize)) self.textItalicsButton.click() if font.italic( ) else self.textItalicsButton.clearFocus( ) #TODO: code smell, review later self.textBoldButton.click() if font.bold( ) else self.textBoldButton.clearFocus( ) #TODO: code smell, review later self.move(pos) self.restoreState(settings.value("mainWindow/state")) def setCurrentPage(self): """Updates the self.currentPage variable""" self.currentPage = self.tabWidget.currentWidget() self.currentPage.I_becameCurrentPage() print("::::::::: New current page set: ", self.currentPage.getNote().NoteName) # TODO: remove def notifyMediatorOfCurrentPageChange(self): self.mediator.setCurrentlyDisplayedNote(self.currentPage.getNote()) def onNewClick(self): """Create a new area to write new note""" tabCount = self.tabWidget.count() + 1 noteObject = self.mediator.createNote(tabCount) page = View.TabbedPage.TabbedPage(self, note=noteObject) newPageIndex = self.tabWidget.addTab( page, "Note {}".format(tabCount)) # add the page to the parent tabWidget page.page_number = newPageIndex # assign a number to the page self.numberOfTabs = self.tabWidget.count() # self.tabWidgetTabBar.moveTab(self.plusTabIndex, # self.numberOfTabs) # move the "+" tab to the end TODO: update the index of the plus tab widget self.moveToTab(newPageIndex) self.currentPage.setFocus() def moveToTab(self, newPageIndex: int): self.tabWidgetTabBar.setCurrentIndex( newPageIndex) # switch to the just created page def onSaveClick(self, pages: list = []): """This method gets the text from the current page; Passes the text along with the attached Note object to the mediator to save """ if len(pages) == 0: # pages is a list of note pages to be saved pages.append(self.currentPage) for page in pages: if page.dirty: rich_text = page.toHtml( ) # get the text from the current document # note = self.currentPage.getNote() # get the Note object attached to this page try: self.mediator.saveNote(rich_text, page.getNote( )) # give the text document to the mediator self.currentPageIsNowDirty(False) except IOError as error: QMessageBox(self, "File Save Error", "Could not save the file", "Ok") assert isinstance( error, IOError ) # pycharm suggested this, I don't think it's needed here print("Error", "IOError occurred", error) logging.log(1, "IOError occurred", error) except NoNoteException as error: # QMessageBox.critical(self, "File Save Error", "Could not save the file: {}".format(error), # "Ok") # TODO:Not sure this will work yet. Handled print("Error", "IOError occurred: No Note", error) logging.log(1, error) else: print("\n-----> No changes made to code") def onRenameNote(self): """Rename a Note""" # TODO : create a rename dialog window later renameDialog = RenameDialog(self) if renameDialog.exec_(): newName = renameDialog.getNewName() try: self.mediator.renameNote(newName) self.tabWidget.setTabText(self.currentPage.page_number, newName) except Exception as e: # TODO: what error exactly print("Error renaming this file: ", e) # TODO: do better, inform the user of the problem raise e def onUndo(self): """Connect to QTextEdit undo slot. A work around for inability to connect a QAction to QTextEdit undo slot. """ self.currentPage.undo() def onRedo(self): """Connect to QTextEdit undo slot. A work around for inability to connect a QAction to QTextEdit undo slot. """ self.currentPage.redo() # cus = CustomSignal() # cus.redoTriggered.connect(QTextEdit.redo) def onFind(self): # TODO: song request Paul I.K. Dairo: Mo wa dupe textSelected = self.currentPage.getDocumentCursor().selectedText() print("Text Selected is: ", textSelected) self.findDialog = FindDialog.FindDialog(self.currentPage, self, textSelected) self.findDialog.show() # self.dockWidget.show() def onHelpAbout(self): self.helpAboutDialog.show() def onHelpHelp(self): self.helpHelpDialog.show() def closeEvent(self, event: QEvent): # TODO: raise a dialog that prompts the user to accept the closing of the application """ :type event: QEvent """ numberOfPages = self.tabWidget.count() widgetList = [ self.tabWidget.widget(widget) for widget in range(numberOfPages) ] # self.onSaveClick(widgetList) # save notes first self.writeSettings() event.accept() def confirmTabCloseAndCloseTab(self, index): pageToRemove = self.tabWidget.widget(index) if pageToRemove.dirty and isinstance(pageToRemove, View.TabbedPage.TabbedPage): print("Tab Close requested") dialog = QMessageBox.warning(self, "Confirm", "Do you want to close the tab?", "Ok", "Cancel", "", 1) if dialog != 0: return self.tabWidget.removeTab(pageToRemove.page_number) self.recentNotes.append(pageToRemove.getNote()) @staticmethod def addActions_(parent, actions: tuple) -> None: for action in actions: parent.addAction(action) @staticmethod def createAction(parent: QObject, text: str, icon: object = None, shortcut: str = None, tooltip: str = None, enabled: bool = False, signal: str = "triggered", slot: object = None) -> object: action = QAction(parent) action.setText(text) if icon: action.setIcon( QIcon(icon)) # icon is expected as a string argument if shortcut: action.setShortcut(shortcut) if tooltip: action.setToolTip(tooltip) if enabled: action.setEnabled(enabled) if slot and signal == "triggered": action.pyqtConfigure(triggered=slot) return action @staticmethod def findNotesInCurrentDirectory(directory: str): if os.path.exists(directory): for path, dirs, files in os.walk(directory): for file in files: if file.endswith(".dn"): fullPath = os.path.join(directory, path, file) yield os.path.normpath(os.path.abspath(fullPath)) def __str__(self): print("I am a note window with id {}".format(self.winId())) def __repr__(self): print(r"I am a note")