示例#1
0
class Q7ComboBox(QComboBox):
    def __init__(self, arg):
        QComboBox.__init__(self, arg)
        self.actorlist = QListWidget()
        self.setModel(self.actorlist.model())
        self.setView(self.actorlist)
        self.view().installEventFilter(self)
        self.parent = None

    def setParent(self, parent):
        self.parent = parent

    def eventFilter(self, o, e):
        if e.type() == QEvent.KeyPress:
            # kmod = e.modifiers()
            kval = e.key()
            if kval in [Qt.Key_Z]:
                path = self.actorlist.currentItem().text()
                actor = self.parent.findPathObject(path)
                self.parent.changeCurrentActor([path, actor])
                return True
            if kval in [Qt.Key_H]:
                path = self.actorlist.currentItem().text()
                actor = self.parent.findPathObject(path)
                self.parent.changeCurrentActor([path, actor])
                self.parent.hideActor(None)
                return True
        return QComboBox.eventFilter(self, o, e)

    def keyPressEvent(self, event):
        kmod = event.modifiers()
        kval = event.key()
        print(kmod, kval)
示例#2
0
 def refresh_profiles(self, list_widget: QListWidget,
                      new_values: typing.List[str]):
     index = self.get_index(list_widget.currentItem(), new_values)
     list_widget.clear()
     list_widget.addItems(new_values)
     if index != -1:
         list_widget.setCurrentRow(index)
示例#3
0
class FileAssociationsWidget(QWidget):
    """Widget to add applications association to file extensions."""

    # This allows validating a single extension entry or a list of comma
    # separated values (eg `*.json` or `*.json,*.txt,MANIFEST.in`)
    _EXTENSIONS_LIST_REGEX = (r'(?:(?:\*{1,1}|\w+)\.\w+)'
                              r'(?:,(?:\*{1,1}|\w+)\.\w+){0,20}')
    sig_data_changed = Signal(dict)

    def __init__(self, parent=None):
        """Widget to add applications association to file extensions."""
        super(FileAssociationsWidget, self).__init__(parent=parent)

        # Variables
        self._data = {}
        self._dlg_applications = None
        self._dlg_input = None
        self._regex = re.compile(self._EXTENSIONS_LIST_REGEX)

        # Widgets
        self.label = QLabel(
            _('Here you can associate which external applications you want'
              'to use to open specific file extensions <br> (e.g. .txt '
              'files with Notepad++ or .csv files with Excel).'))
        self.label_extensions = QLabel(_('File types:'))
        self.list_extensions = QListWidget()
        self.button_add = QPushButton(_('Add'))
        self.button_remove = QPushButton(_('Remove'))
        self.button_edit = QPushButton(_('Edit'))

        self.label_applications = QLabel(_('Associated applications:'))
        self.list_applications = QListWidget()
        self.button_add_application = QPushButton(_('Add'))
        self.button_remove_application = QPushButton(_('Remove'))
        self.button_default = QPushButton(_('Set default'))

        # Layout
        layout_extensions = QHBoxLayout()
        layout_extensions.addWidget(self.list_extensions, 4)

        layout_buttons_extensions = QVBoxLayout()
        layout_buttons_extensions.addWidget(self.button_add)
        layout_buttons_extensions.addWidget(self.button_remove)
        layout_buttons_extensions.addWidget(self.button_edit)
        layout_buttons_extensions.addStretch()

        layout_applications = QHBoxLayout()
        layout_applications.addWidget(self.list_applications, 4)

        layout_buttons_applications = QVBoxLayout()
        layout_buttons_applications.addWidget(self.button_add_application)
        layout_buttons_applications.addWidget(self.button_remove_application)
        layout_buttons_applications.addWidget(self.button_default)
        layout_buttons_applications.addStretch()

        layout_extensions.addLayout(layout_buttons_extensions, 2)
        layout_applications.addLayout(layout_buttons_applications, 2)

        layout = QVBoxLayout()
        layout.addWidget(self.label)
        layout.addWidget(self.label_extensions)
        layout.addLayout(layout_extensions)
        layout.addWidget(self.label_applications)
        layout.addLayout(layout_applications)

        self.setLayout(layout)

        # Signals
        self.button_add.clicked.connect(lambda: self.add_association())
        self.button_remove.clicked.connect(self.remove_association)
        self.button_edit.clicked.connect(self.edit_association)
        self.button_add_application.clicked.connect(self.add_application)
        self.button_remove_application.clicked.connect(self.remove_application)
        self.button_default.clicked.connect(self.set_default_application)
        self.list_extensions.currentRowChanged.connect(self.update_extensions)
        self.list_extensions.itemDoubleClicked.connect(self.edit_association)
        self.list_applications.currentRowChanged.connect(
            self.update_applications)
        self._refresh()
        self._create_association_dialog()

    def _refresh(self):
        """Refresh the status of buttons on widget."""
        self.setUpdatesEnabled(False)
        for widget in [
                self.button_remove, self.button_add_application,
                self.button_edit, self.button_remove_application,
                self.button_default
        ]:
            widget.setDisabled(True)

        item = self.list_extensions.currentItem()
        if item:
            for widget in [
                    self.button_remove, self.button_add_application,
                    self.button_remove_application, self.button_edit
            ]:
                widget.setDisabled(False)
        self.update_applications()
        self.setUpdatesEnabled(True)

    def _add_association(self, value):
        """Add association helper."""
        # Check value is not pressent
        for row in range(self.list_extensions.count()):
            item = self.list_extensions.item(row)
            if item.text().strip() == value.strip():
                break
        else:
            item = QListWidgetItem(value)
            self.list_extensions.addItem(item)
            self.list_extensions.setCurrentItem(item)

        self._refresh()

    def _add_application(self, app_name, fpath):
        """Add application helper."""
        app_not_found_text = _(' (Application not found!)')
        for row in range(self.list_applications.count()):
            item = self.list_applications.item(row)
            # Ensure the actual name is checked without the `app not found`
            # additional text, in case app was not found
            item_text = item.text().replace(app_not_found_text, '').strip()
            if item and item_text == app_name:
                break
        else:
            icon = get_application_icon(fpath)

            if not (os.path.isfile(fpath) or os.path.isdir(fpath)):
                app_name += app_not_found_text

            item = QListWidgetItem(icon, app_name)
            self.list_applications.addItem(item)
            self.list_applications.setCurrentItem(item)

        if not (os.path.isfile(fpath) or os.path.isdir(fpath)):
            item.setToolTip(_('Application not found!'))

    def _update_extensions(self):
        """Update extensions list."""
        self.list_extensions.clear()
        for extension, _ in sorted(self._data.items()):
            self._add_association(extension)

        # Select first item
        self.list_extensions.setCurrentRow(0)
        self.update_extensions()
        self.update_applications()

    def _create_association_dialog(self):
        """Create input extension dialog and save it to for reuse."""
        self._dlg_input = InputTextDialog(
            self,
            title=_('File association'),
            label=(_('Enter new file extension. You can add several values '
                     'separated by commas.<br>Examples include:') +
                   '<ul><li><code>*.txt</code></li>' +
                   '<li><code>*.json,*,csv</code></li>' +
                   '<li><code>*.json,README.md</code></li></ul>'),
        )
        self._dlg_input.set_regex_validation(self._EXTENSIONS_LIST_REGEX)

    def load_values(self, data=None):
        """
        Load file associations data.

        Format {'*.ext': [['Application Name', '/path/to/app/executable']]}

        `/path/to/app/executable` is an executable app on mac and windows and
        a .desktop xdg file on linux.
        """
        self._data = data
        self._update_extensions()

    def add_association(self, value=None):
        """Add extension file association."""
        if value is None:
            text, ok_pressed = '', False
            self._dlg_input.set_text('')

            if self._dlg_input.exec_():
                text = self._dlg_input.text()
                ok_pressed = True
        else:
            match = self._regex.match(value)
            text, ok_pressed = value, bool(match)

        if ok_pressed:
            if text not in self._data:
                self._data[text] = []
                self._add_association(text)
                self.check_data_changed()

    def remove_association(self):
        """Remove extension file association."""
        if self._data:
            if self.current_extension:
                self._data.pop(self.current_extension)
                self._update_extensions()
                self._refresh()
                self.check_data_changed()

    def edit_association(self):
        """Edit text of current selected association."""
        old_text = self.current_extension
        self._dlg_input.set_text(old_text)

        if self._dlg_input.exec_():
            new_text = self._dlg_input.text()
            if old_text != new_text:
                values = self._data.pop(self.current_extension)
                self._data[new_text] = values
                self._update_extensions()
                self._refresh()
                for row in range(self.list_extensions.count()):
                    item = self.list_extensions.item(row)
                    if item.text() == new_text:
                        self.list_extensions.setCurrentItem(item)
                        break
                self.check_data_changed()

    def add_application(self):
        """Remove application to selected extension."""
        if self.current_extension:
            if self._dlg_applications is None:
                self._dlg_applications = ApplicationsDialog(self)

            self._dlg_applications.set_extension(self.current_extension)

            if self._dlg_applications.exec_():
                app_name = self._dlg_applications.application_name
                fpath = self._dlg_applications.application_path
                self._data[self.current_extension].append((app_name, fpath))
                self._add_application(app_name, fpath)
                self.check_data_changed()

    def remove_application(self):
        """Remove application from selected extension."""
        current_row = self.list_applications.currentRow()
        values = self._data.get(self.current_extension)
        if values and current_row != -1:
            values.pop(current_row)
            self.update_extensions()
            self.update_applications()
            self.check_data_changed()

    def set_default_application(self):
        """
        Set the selected item on the application list as default application.
        """
        current_row = self.list_applications.currentRow()
        if current_row != -1:
            values = self._data[self.current_extension]
            value = values.pop(current_row)
            values.insert(0, value)
            self._data[self.current_extension] = values
            self.update_extensions()
            self.check_data_changed()

    def update_extensions(self, row=None):
        """Update extensiosn list after additions or deletions."""
        self.list_applications.clear()
        for extension, values in self._data.items():
            if extension.strip() == self.current_extension:
                for (app_name, fpath) in values:
                    self._add_application(app_name, fpath)
                break
        self.list_applications.setCurrentRow(0)
        self._refresh()

    def update_applications(self, row=None):
        """Update application list after additions or deletions."""
        current_row = self.list_applications.currentRow()
        self.button_default.setEnabled(current_row != 0)

    def check_data_changed(self):
        """Check if data has changed and emit signal as needed."""
        self.sig_data_changed.emit(self._data)

    @property
    def current_extension(self):
        """Return the current selected extension text."""
        item = self.list_extensions.currentItem()
        if item:
            return item.text()

    @property
    def data(self):
        """Return the current file associations data."""
        return self._data.copy()
示例#4
0
class PathManager(QDialog):
    redirect_stdio = Signal(bool)
    
    def __init__(self, parent=None, pathlist=None, ro_pathlist=None,
                 not_active_pathlist=None, sync=True):
        QDialog.__init__(self, parent)
        
        # Destroying the C++ object right after closing the dialog box,
        # otherwise it may be garbage-collected in another QThread
        # (e.g. the editor's analysis thread in Spyder), thus leading to
        # a segmentation fault on UNIX or an application crash on Windows
        self.setAttribute(Qt.WA_DeleteOnClose)
        
        assert isinstance(pathlist, list)
        self.pathlist = pathlist
        if not_active_pathlist is None:
            not_active_pathlist = []
        self.not_active_pathlist = not_active_pathlist
        if ro_pathlist is None:
            ro_pathlist = []
        self.ro_pathlist = ro_pathlist
        
        self.last_path = getcwd()
        
        self.setWindowTitle(_("PYTHONPATH manager"))
        self.setWindowIcon(ima.icon('pythonpath'))
        self.resize(500, 300)
        
        self.selection_widgets = []
        
        layout = QVBoxLayout()
        self.setLayout(layout)
        
        top_layout = QHBoxLayout()
        layout.addLayout(top_layout)
        self.toolbar_widgets1 = self.setup_top_toolbar(top_layout)

        self.listwidget = QListWidget(self)
        self.listwidget.currentRowChanged.connect(self.refresh)
        self.listwidget.itemChanged.connect(self.update_not_active_pathlist)
        layout.addWidget(self.listwidget)

        bottom_layout = QHBoxLayout()
        layout.addLayout(bottom_layout)
        self.sync_button = None
        self.toolbar_widgets2 = self.setup_bottom_toolbar(bottom_layout, sync)        
        
        # Buttons configuration
        bbox = QDialogButtonBox(QDialogButtonBox.Close)
        bbox.rejected.connect(self.reject)
        bottom_layout.addWidget(bbox)
        
        self.update_list()
        self.refresh()

    @property
    def active_pathlist(self):
        return [path for path in self.pathlist
                if path not in self.not_active_pathlist]

    def _add_widgets_to_layout(self, layout, widgets):
        layout.setAlignment(Qt.AlignLeft)
        for widget in widgets:
            layout.addWidget(widget)
        
    def setup_top_toolbar(self, layout):
        toolbar = []
        movetop_button = create_toolbutton(self,
                                    text=_("Move to top"),
                                    icon=ima.icon('2uparrow'),
                                    triggered=lambda: self.move_to(absolute=0),
                                    text_beside_icon=True)
        toolbar.append(movetop_button)
        moveup_button = create_toolbutton(self,
                                    text=_("Move up"),
                                    icon=ima.icon('1uparrow'),
                                    triggered=lambda: self.move_to(relative=-1),
                                    text_beside_icon=True)
        toolbar.append(moveup_button)
        movedown_button = create_toolbutton(self,
                                    text=_("Move down"),
                                    icon=ima.icon('1downarrow'),
                                    triggered=lambda: self.move_to(relative=1),
                                    text_beside_icon=True)
        toolbar.append(movedown_button)
        movebottom_button = create_toolbutton(self,
                                    text=_("Move to bottom"),
                                    icon=ima.icon('2downarrow'),
                                    triggered=lambda: self.move_to(absolute=1),
                                    text_beside_icon=True)
        toolbar.append(movebottom_button)
        self.selection_widgets.extend(toolbar)
        self._add_widgets_to_layout(layout, toolbar)
        return toolbar
    
    def setup_bottom_toolbar(self, layout, sync=True):
        toolbar = []
        add_button = create_toolbutton(self, text=_('Add path'),
                                       icon=ima.icon('edit_add'),
                                       triggered=self.add_path,
                                       text_beside_icon=True)
        toolbar.append(add_button)
        remove_button = create_toolbutton(self, text=_('Remove path'),
                                          icon=ima.icon('edit_remove'),
                                          triggered=self.remove_path,
                                          text_beside_icon=True)
        toolbar.append(remove_button)
        self.selection_widgets.append(remove_button)
        self._add_widgets_to_layout(layout, toolbar)
        layout.addStretch(1)
        if os.name == 'nt' and sync:
            self.sync_button = create_toolbutton(self,
                  text=_("Synchronize..."),
                  icon=ima.icon('fileimport'), triggered=self.synchronize,
                  tip=_("Synchronize Spyder's path list with PYTHONPATH "
                              "environment variable"),
                  text_beside_icon=True)
            layout.addWidget(self.sync_button)
        return toolbar

    @Slot()
    def synchronize(self):
        """
        Synchronize Spyder's path list with PYTHONPATH environment variable
        Only apply to: current user, on Windows platforms
        """
        answer = QMessageBox.question(self, _("Synchronize"),
            _("This will synchronize Spyder's path list with "
                    "<b>PYTHONPATH</b> environment variable for current user, "
                    "allowing you to run your Python modules outside Spyder "
                    "without having to configure sys.path. "
                    "<br>Do you want to clear contents of PYTHONPATH before "
                    "adding Spyder's path list?"),
            QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
        if answer == QMessageBox.Cancel:
            return
        elif answer == QMessageBox.Yes:
            remove = True
        else:
            remove = False
        from spyder.utils.environ import (get_user_env, set_user_env,
                                          listdict2envdict)
        env = get_user_env()
        if remove:
            ppath = self.active_pathlist+self.ro_pathlist
        else:
            ppath = env.get('PYTHONPATH', [])
            if not isinstance(ppath, list):
                ppath = [ppath]
            ppath = [path for path in ppath
                     if path not in (self.active_pathlist+self.ro_pathlist)]
            ppath.extend(self.active_pathlist+self.ro_pathlist)
        env['PYTHONPATH'] = ppath
        set_user_env(listdict2envdict(env), parent=self)

    def get_path_list(self):
        """Return path list (does not include the read-only path list)"""
        return self.pathlist

    def update_not_active_pathlist(self, item):
        path = item.text()
        if bool(item.checkState()) is True:
            self.remove_from_not_active_pathlist(path)
        else:
            self.add_to_not_active_pathlist(path)

    def add_to_not_active_pathlist(self, path):
        if path not in self.not_active_pathlist:
            self.not_active_pathlist.append(path)

    def remove_from_not_active_pathlist(self, path):
        if path in self.not_active_pathlist:
            self.not_active_pathlist.remove(path)
        
    def update_list(self):
        """Update path list"""
        self.listwidget.clear()
        for name in self.pathlist+self.ro_pathlist:
            item = QListWidgetItem(name)
            item.setIcon(ima.icon('DirClosedIcon'))
            if name in self.ro_pathlist:
                item.setFlags(Qt.NoItemFlags | Qt.ItemIsUserCheckable)
                item.setCheckState(Qt.Checked)
            elif name in self.not_active_pathlist:
                item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
                item.setCheckState(Qt.Unchecked)
            else:
                item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
                item.setCheckState(Qt.Checked)
            self.listwidget.addItem(item)
        self.refresh()
        
    def refresh(self, row=None):
        """Refresh widget"""
        for widget in self.selection_widgets:
            widget.setEnabled(self.listwidget.currentItem() is not None)
        not_empty = self.listwidget.count() > 0
        if self.sync_button is not None:
            self.sync_button.setEnabled(not_empty)
    
    def move_to(self, absolute=None, relative=None):
        index = self.listwidget.currentRow()
        if absolute is not None:
            if absolute:
                new_index = len(self.pathlist)-1
            else:
                new_index = 0
        else:
            new_index = index + relative        
        new_index = max(0, min(len(self.pathlist)-1, new_index))
        path = self.pathlist.pop(index)
        self.pathlist.insert(new_index, path)
        self.update_list()
        self.listwidget.setCurrentRow(new_index)

    @Slot()
    def remove_path(self):
        answer = QMessageBox.warning(self, _("Remove path"),
            _("Do you really want to remove selected path?"),
            QMessageBox.Yes | QMessageBox.No)
        if answer == QMessageBox.Yes:
            self.pathlist.pop(self.listwidget.currentRow())
            self.remove_from_not_active_pathlist(
                    self.listwidget.currentItem().text())
            self.update_list()

    @Slot()
    def add_path(self):
        self.redirect_stdio.emit(False)
        directory = getexistingdirectory(self, _("Select directory"),
                                         self.last_path)
        self.redirect_stdio.emit(True)
        if directory:
            directory = osp.abspath(directory)
            self.last_path = directory
            if directory in self.pathlist:
                item = self.listwidget.findItems(directory, Qt.MatchExactly)[0]
                item.setCheckState(Qt.Checked)
                answer = QMessageBox.question(self, _("Add path"),
                    _("This directory is already included in Spyder path "
                            "list.<br>Do you want to move it to the top of "
                            "the list?"),
                    QMessageBox.Yes | QMessageBox.No)
                if answer == QMessageBox.Yes:
                    self.pathlist.remove(directory)
                else:
                    return
            self.pathlist.insert(0, directory)
            self.update_list()
示例#5
0
class MeasurementSettings(QWidget):
    """
    :type settings: Settings
    """
    def __init__(self, settings: PartSettings, parent=None):
        super().__init__(parent)
        self.chosen_element: Optional[MeasurementListWidgetItem] = None
        self.chosen_element_area: Optional[Tuple[AreaType, float]] = None
        self.settings = settings
        self.profile_list = QListWidget(self)
        self.profile_description = QTextEdit(self)
        self.profile_description.setReadOnly(True)
        self.profile_options = QListWidget()
        self.profile_options_chosen = QListWidget()
        self.measurement_area_choose = QEnumComboBox(enum_class=AreaType)
        self.per_component = QEnumComboBox(enum_class=PerComponent)
        self.power_num = QDoubleSpinBox()
        self.power_num.setDecimals(3)
        self.power_num.setRange(-100, 100)
        self.power_num.setValue(1)
        self.choose_butt = QPushButton("→", self)
        self.discard_butt = QPushButton("←", self)
        self.proportion_butt = QPushButton("Ratio", self)
        self.proportion_butt.setToolTip("Create proportion from two parameter")
        self.move_up = QPushButton("↑", self)
        self.move_down = QPushButton("↓", self)
        self.remove_button = QPushButton("Remove")
        self.save_butt = QPushButton("Save")
        self.save_butt.setToolTip(
            "Set name for set and choose at least one parameter")
        self.save_butt_with_name = QPushButton(
            "Save with custom parameters designation")
        self.save_butt_with_name.setToolTip(
            "Set name for set and choose at least one parameter")
        self.reset_butt = QPushButton("Clear")
        self.soft_reset_butt = QPushButton("Remove user parameters")
        self.profile_name = QLineEdit(self)

        self.delete_profile_butt = QPushButton("Delete ")
        self.export_profiles_butt = QPushButton("Export")
        self.import_profiles_butt = QPushButton("Import")
        self.edit_profile_butt = QPushButton("Edit")

        self.choose_butt.setDisabled(True)
        self.choose_butt.clicked.connect(self.choose_option)
        self.discard_butt.setDisabled(True)
        self.discard_butt.clicked.connect(self.discard_option)
        self.proportion_butt.setDisabled(True)
        self.proportion_butt.clicked.connect(self.proportion_action)
        self.save_butt.setDisabled(True)
        self.save_butt.clicked.connect(self.save_action)
        self.save_butt_with_name.setDisabled(True)
        self.save_butt_with_name.clicked.connect(self.named_save_action)
        self.profile_name.textChanged.connect(self.name_changed)
        self.move_down.setDisabled(True)
        self.move_down.clicked.connect(self.move_down_fun)
        self.move_up.setDisabled(True)
        self.move_up.clicked.connect(self.move_up_fun)
        self.remove_button.setDisabled(True)
        self.remove_button.clicked.connect(self.remove_element)
        self.reset_butt.clicked.connect(self.reset_action)
        self.soft_reset_butt.clicked.connect(self.soft_reset)
        self.delete_profile_butt.setDisabled(True)
        self.delete_profile_butt.clicked.connect(self.delete_profile)
        self.export_profiles_butt.clicked.connect(
            self.export_measurement_profiles)
        self.import_profiles_butt.clicked.connect(
            self.import_measurement_profiles)
        self.edit_profile_butt.clicked.connect(self.edit_profile)

        self.profile_list.itemSelectionChanged.connect(self.profile_chosen)
        self.profile_options.itemSelectionChanged.connect(
            self.create_selection_changed)
        self.profile_options_chosen.itemSelectionChanged.connect(
            self.create_selection_chosen_changed)
        self.settings.measurement_profiles_changed.connect(
            self._refresh_profiles)

        layout = QVBoxLayout()
        layout.addWidget(QLabel("Measurement set:"))
        profile_layout = QHBoxLayout()
        profile_layout.addWidget(self.profile_list)
        profile_layout.addWidget(self.profile_description)
        profile_buttons_layout = QHBoxLayout()
        profile_buttons_layout.addWidget(self.delete_profile_butt)
        profile_buttons_layout.addWidget(self.export_profiles_butt)
        profile_buttons_layout.addWidget(self.import_profiles_butt)
        profile_buttons_layout.addWidget(self.edit_profile_butt)
        profile_buttons_layout.addStretch()
        layout.addLayout(profile_layout)
        layout.addLayout(profile_buttons_layout)
        heading_layout = QHBoxLayout()
        # heading_layout.addWidget(QLabel("Create profile"), 1)
        heading_layout.addWidget(h_line(), 6)
        layout.addLayout(heading_layout)
        name_layout = QHBoxLayout()
        name_layout.addWidget(QLabel("Set name:"))
        name_layout.addWidget(self.profile_name)
        name_layout.addStretch()
        name_layout.addWidget(QLabel("Per component:"))
        name_layout.addWidget(self.per_component)
        name_layout.addWidget(QLabel("Area:"))
        name_layout.addWidget(self.measurement_area_choose)
        name_layout.addWidget(QLabel("to power:"))
        name_layout.addWidget(self.power_num)
        layout.addLayout(name_layout)
        create_layout = QHBoxLayout()
        create_layout.addWidget(self.profile_options)
        butt_op_layout = QVBoxLayout()
        butt_op_layout.addStretch()
        butt_op_layout.addWidget(self.choose_butt)
        butt_op_layout.addWidget(self.discard_butt)
        butt_op_layout.addWidget(self.proportion_butt)
        butt_op_layout.addWidget(self.reset_butt)
        butt_op_layout.addStretch()
        create_layout.addLayout(butt_op_layout)
        create_layout.addWidget(self.profile_options_chosen)
        butt_move_layout = QVBoxLayout()
        butt_move_layout.addStretch()
        butt_move_layout.addWidget(self.move_up)
        butt_move_layout.addWidget(self.move_down)
        butt_move_layout.addWidget(self.remove_button)
        butt_move_layout.addStretch()
        create_layout.addLayout(butt_move_layout)
        layout.addLayout(create_layout)
        save_butt_layout = QHBoxLayout()
        save_butt_layout.addWidget(self.soft_reset_butt)
        save_butt_layout.addStretch()
        save_butt_layout.addWidget(self.save_butt)
        save_butt_layout.addWidget(self.save_butt_with_name)
        layout.addLayout(save_butt_layout)
        self.setLayout(layout)

        for profile in MEASUREMENT_DICT.values():
            help_text = profile.get_description()
            lw = MeasurementListWidgetItem(profile.get_starting_leaf())
            lw.setToolTip(help_text)
            self.profile_options.addItem(lw)
        self._refresh_profiles()

    def _refresh_profiles(self):
        item = self.profile_list.currentItem()
        items = list(self.settings.measurement_profiles.keys())
        try:
            index = items.index(item.text())
        except (ValueError, AttributeError):
            index = -1

        self.profile_list.clear()
        self.profile_list.addItems(items)
        self.profile_list.setCurrentRow(index)

        if self.profile_list.count() == 0:
            self.export_profiles_butt.setDisabled(True)
        else:
            self.export_profiles_butt.setEnabled(True)

    def remove_element(self):
        elem = self.profile_options_chosen.currentItem()
        if elem is None:
            return
        index = self.profile_options_chosen.currentRow()
        self.profile_options_chosen.takeItem(index)
        if self.profile_options_chosen.count() == 0:
            self.move_down.setDisabled(True)
            self.move_up.setDisabled(True)
            self.remove_button.setDisabled(True)
            self.discard_butt.setDisabled(True)
            self.save_butt.setDisabled(True)
            self.save_butt_with_name.setDisabled(True)

    def delete_profile(self):
        item = self.profile_list.currentItem()
        del self.settings.measurement_profiles[str(item.text())]

    def profile_chosen(self):
        self.delete_profile_butt.setEnabled(True)
        if self.profile_list.count() == 0:
            self.profile_description.setText("")
            return
        item = self.profile_list.currentItem()
        if item is None:
            self.profile_description.setText("")
            return
        profile = self.settings.measurement_profiles[item.text()]
        self.profile_description.setText(str(profile))

    def create_selection_changed(self):
        self.choose_butt.setEnabled(True)
        self.proportion_butt.setEnabled(True)

    def proportion_action(self):
        # TODO use get_parameters
        if self.chosen_element is None:
            item = self.profile_options.currentItem()
            self.chosen_element_area = self.get_parameters(
                deepcopy(item.stat),
                self.measurement_area_choose.currentEnum(),
                self.per_component.currentEnum(),
                self.power_num.value(),
            )
            if self.chosen_element_area is None:
                return
            self.chosen_element = item
            item.setIcon(QIcon(os.path.join(icons_dir, "task-accepted.png")))
        elif (self.profile_options.currentItem() == self.chosen_element
              and self.measurement_area_choose.currentEnum()
              == self.chosen_element_area.area
              and self.per_component.currentEnum()
              == self.chosen_element_area.per_component):
            self.chosen_element.setIcon(QIcon())
            self.chosen_element = None
        else:
            item: MeasurementListWidgetItem = self.profile_options.currentItem(
            )
            leaf = self.get_parameters(
                deepcopy(item.stat),
                self.measurement_area_choose.currentEnum(),
                self.per_component.currentEnum(),
                self.power_num.value(),
            )
            if leaf is None:
                return
            lw = MeasurementListWidgetItem(
                Node(op="/", left=self.chosen_element_area, right=leaf))
            lw.setToolTip("User defined")
            self._add_option(lw)
            self.chosen_element.setIcon(QIcon())
            self.chosen_element = None
            self.chosen_element_area = None

    def _add_option(self, item: MeasurementListWidgetItem):
        for i in range(self.profile_options_chosen.count()):
            if item.text() == self.profile_options_chosen.item(i).text():
                return
        self.profile_options_chosen.addItem(item)
        if self.good_name():
            self.save_butt.setEnabled(True)
            self.save_butt_with_name.setEnabled(True)
        if self.profile_options.count() == 0:
            self.choose_butt.setDisabled(True)

    def create_selection_chosen_changed(self):
        # print(self.profile_options_chosen.count())
        self.remove_button.setEnabled(True)
        if self.profile_options_chosen.count() == 0:
            self.move_down.setDisabled(True)
            self.move_up.setDisabled(True)
            self.remove_button.setDisabled(True)
            return
        self.discard_butt.setEnabled(True)
        if self.profile_options_chosen.currentRow() != 0:
            self.move_up.setEnabled(True)
        else:
            self.move_up.setDisabled(True)
        if self.profile_options_chosen.currentRow(
        ) != self.profile_options_chosen.count() - 1:
            self.move_down.setEnabled(True)
        else:
            self.move_down.setDisabled(True)

    def good_name(self):
        return str(self.profile_name.text()).strip() != ""

    def move_down_fun(self):
        row = self.profile_options_chosen.currentRow()
        item = self.profile_options_chosen.takeItem(row)
        self.profile_options_chosen.insertItem(row + 1, item)
        self.profile_options_chosen.setCurrentRow(row + 1)
        self.create_selection_chosen_changed()

    def move_up_fun(self):
        row = self.profile_options_chosen.currentRow()
        item = self.profile_options_chosen.takeItem(row)
        self.profile_options_chosen.insertItem(row - 1, item)
        self.profile_options_chosen.setCurrentRow(row - 1)
        self.create_selection_chosen_changed()

    def name_changed(self):
        if self.good_name() and self.profile_options_chosen.count() > 0:
            self.save_butt.setEnabled(True)
            self.save_butt_with_name.setEnabled(True)
        else:
            self.save_butt.setDisabled(True)
            self.save_butt_with_name.setDisabled(True)

    def form_dialog(self, arguments):
        return FormDialog(arguments, settings=self.settings, parent=self)

    def get_parameters(self, node: Union[Node, Leaf], area: AreaType,
                       component: PerComponent, power: float):
        if isinstance(node, Node):
            return node
        node = node.replace_(power=power)
        if node.area is None:
            node = node.replace_(area=area)
        if node.per_component is None:
            node = node.replace_(per_component=component)
        with suppress(KeyError):
            arguments = MEASUREMENT_DICT[str(node.name)].get_fields()
            if len(arguments) > 0 and len(node.dict) == 0:
                dial = self.form_dialog(arguments)
                if dial.exec_():
                    node = node._replace(dict=dial.get_values())
                else:
                    return
        return node

    def choose_option(self):
        selected_item = self.profile_options.currentItem()
        # selected_row = self.profile_options.currentRow()
        if not isinstance(selected_item, MeasurementListWidgetItem):
            raise ValueError(
                f"Current item (type: {type(selected_item)} is not instance of MeasurementListWidgetItem"
            )
        node = deepcopy(selected_item.stat)
        # noinspection PyTypeChecker
        node = self.get_parameters(node,
                                   self.measurement_area_choose.currentEnum(),
                                   self.per_component.currentEnum(),
                                   self.power_num.value())
        if node is None:
            return
        lw = MeasurementListWidgetItem(node)
        lw.setToolTip(selected_item.toolTip())
        self._add_option(lw)

    def discard_option(self):
        selected_item: MeasurementListWidgetItem = self.profile_options_chosen.currentItem(
        )
        #  selected_row = self.profile_options_chosen.currentRow()
        lw = MeasurementListWidgetItem(deepcopy(selected_item.stat))
        lw.setToolTip(selected_item.toolTip())
        self.create_selection_chosen_changed()
        for i in range(self.profile_options.count()):
            if lw.text() == self.profile_options.item(i).text():
                return
        self.profile_options.addItem(lw)

    def edit_profile(self):
        item = self.profile_list.currentItem()
        if item is None:
            return
        profile = self.settings.measurement_profiles[str(item.text())]
        self.profile_options_chosen.clear()
        self.profile_name.setText(item.text())
        for ch in profile.chosen_fields:
            self.profile_options_chosen.addItem(
                MeasurementListWidgetItem(ch.calculation_tree))
        # self.gauss_img.setChecked(profile.use_gauss_image)
        self.save_butt.setEnabled(True)
        self.save_butt_with_name.setEnabled(True)

    def save_action(self):
        if self.profile_name.text() in self.settings.measurement_profiles:
            ret = QMessageBox.warning(
                self,
                "Profile exist",
                "Profile exist\nWould you like to overwrite it?",
                QMessageBox.No | QMessageBox.Yes,
            )
            if ret == QMessageBox.No:
                return
        selected_values = []
        for i in range(self.profile_options_chosen.count()):
            element: MeasurementListWidgetItem = self.profile_options_chosen.item(
                i)
            selected_values.append(
                MeasurementEntry(element.text(), element.stat))
        stat_prof = MeasurementProfile(self.profile_name.text(),
                                       selected_values)
        self.settings.measurement_profiles[stat_prof.name] = stat_prof
        self.settings.dump()
        self.export_profiles_butt.setEnabled(True)

    def named_save_action(self):
        if self.profile_name.text() in self.settings.measurement_profiles:
            ret = QMessageBox.warning(
                self,
                "Profile exist",
                "Profile exist\nWould you like to overwrite it?",
                QMessageBox.No | QMessageBox.Yes,
            )
            if ret == QMessageBox.No:
                return
        selected_values = []
        for i in range(self.profile_options_chosen.count()):
            txt = str(self.profile_options_chosen.item(i).text())
            selected_values.append((txt, str, txt))
        val_dialog = MultipleInput("Set fields name",
                                   list(selected_values),
                                   parent=self)
        if val_dialog.exec_():
            selected_values = []
            for i in range(self.profile_options_chosen.count()):
                element: MeasurementListWidgetItem = self.profile_options_chosen.item(
                    i)
                selected_values.append(
                    MeasurementEntry(val_dialog.result[element.text()],
                                     element.stat))
            stat_prof = MeasurementProfile(self.profile_name.text(),
                                           selected_values)
            self.settings.measurement_profiles[stat_prof.name] = stat_prof
            self.export_profiles_butt.setEnabled(True)

    def reset_action(self):
        self.profile_options.clear()
        self.profile_options_chosen.clear()
        self.profile_name.setText("")
        self.save_butt.setDisabled(True)
        self.save_butt_with_name.setDisabled(True)
        self.move_down.setDisabled(True)
        self.move_up.setDisabled(True)
        self.proportion_butt.setDisabled(True)
        self.choose_butt.setDisabled(True)
        self.discard_butt.setDisabled(True)
        for profile in MEASUREMENT_DICT.values():
            help_text = profile.get_description()
            lw = MeasurementListWidgetItem(profile.get_starting_leaf())
            lw.setToolTip(help_text)
            self.profile_options.addItem(lw)

    def soft_reset(self):
        # TODO rim should not be removed
        shift = 0
        for i in range(self.profile_options.count()):
            item = self.profile_options.item(i - shift)
            if str(item.text()) not in MEASUREMENT_DICT:
                self.profile_options.takeItem(i - shift)
                if item == self.chosen_element:
                    self.chosen_element = None
                del item
                shift += 1
        self.create_selection_changed()

    def export_measurement_profiles(self):
        exp = ExportDialog(self.settings.measurement_profiles,
                           StringViewer,
                           parent=self)
        if not exp.exec_():
            return
        dial = PSaveDialog(
            "Measurement profile (*.json)",
            settings=self.settings,
            path="io.export_directory",
            caption="Export settings profiles",
        )
        dial.selectFile("measurements_profile.json")

        if dial.exec_():
            file_path = str(dial.selectedFiles()[0])
            data = {
                x: self.settings.measurement_profiles[x]
                for x in exp.get_export_list()
            }
            with open(file_path, "w", encoding="utf-8") as ff:
                json.dump(data,
                          ff,
                          cls=self.settings.json_encoder_class,
                          indent=2)

    def import_measurement_profiles(self):
        dial = PLoadDialog(
            "Measurement profile (*.json)",
            settings=self.settings,
            path="io.export_directory",
            caption="Import settings profiles",
            parent=self,
        )
        if dial.exec_():
            file_path = str(dial.selectedFiles()[0])
            stat, err = self.settings.load_part(file_path)
            if err:
                QMessageBox.warning(
                    self, "Import error",
                    "error during importing, part of data were filtered.")
            measurement_dict = self.settings.measurement_profiles
            imp = ImportDialog(stat, measurement_dict, StringViewer)
            if not imp.exec_():
                return
            for original_name, final_name in imp.get_import_list():
                measurement_dict[final_name] = stat[original_name]
            self.settings.dump()
示例#6
0
class PMGListCtrl(BaseExtendedWidget):
    def __init__(self,
                 layout_dir: str,
                 title: str,
                 initial_value: List[List[str]],
                 new_id_func: Callable = None):
        super().__init__(layout_dir)
        self.choices = []
        self.text_list = []
        lab_title = QLabel(text=title)
        layout = QHBoxLayout()
        self.central_layout.addWidget(lab_title)
        self.on_check_callback = None
        self.list_widget = QListWidget()
        self.list_widget.mouseDoubleClickEvent = self.on_listwidget_double_cicked

        self.set_value(initial_value)
        layout_tools = QVBoxLayout()
        self.button_add_item = QPushButton('+')
        self.button_delete_item = QPushButton('-')
        self.button_delete_item.clicked.connect(self.delete_row)
        self.button_add_item.clicked.connect(self.add_row)
        self.button_add_item.setMaximumWidth(20)
        self.button_delete_item.setMaximumWidth(20)
        layout_tools.addWidget(self.button_add_item)
        layout_tools.addWidget(self.button_delete_item)
        layout.addLayout(layout_tools)
        layout.addWidget(self.list_widget)
        self.central_layout.addLayout(layout)
        self.data = initial_value
        self.new_id_func = new_id_func

        self.text_edit = QLineEdit(parent=self.list_widget)
        self.text_edit.setWindowFlags(self.text_edit.windowFlags() | Qt.Dialog
                                      | Qt.FramelessWindowHint)
        self.text_edit.hide()
        self.completer = QCompleter()
        self.text_edit.setCompleter(self.completer)
        self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)

    def set_completions(self, completions: List[str]):
        """
        设置补全内容
        Args:
            completions:

        Returns:

        """
        self.completer.setModel(QStringListModel(completions))

    def new_id(self):
        if callable(self.new_id_func):
            return self.new_id_func()
        else:
            return None

    def add_row(self):
        self.data = self.get_value()
        self.data[0].append(self.new_id())
        self.data[1].append('Unnamed')
        self.list_widget.addItem(QListWidgetItem('Unnamed'))

    def delete_row(self):
        index = self.list_widget.currentIndex().row()
        self.data[0].pop(index)
        self.data[1].pop(index)
        self.list_widget.takeItem(index)

    def on_listwidget_double_cicked(self, evt: QMouseEvent):
        print('edit', evt)
        pos = evt.globalPos()
        current_item: QListWidgetItem = self.list_widget.currentItem()

        def set_value():
            current_item.setText(self.text_edit.text())
            self.text_edit.hide()
            self.text_edit.returnPressed.disconnect(set_value)

        item: QListWidgetItem = self.list_widget.currentItem()

        self.text_edit.setGeometry(pos.x(), pos.y(), 200, 20)
        self.text_edit.returnPressed.connect(set_value)
        self.text_edit.show()
        # self.list_widget.editItem(item)

    def get_value(self):
        text = []
        for i in range(self.list_widget.count()):
            text.append(self.list_widget.item(i).text())
        self.data[1] = text
        assert len(self.data[1]) == len(self.data[0]), repr(self.data)
        return self.data

    def set_value(self, data: List[List[str]]):
        self.list_widget.clear()
        self.list_widget.addItems(data[1])
        self.data = data
        for index in range(self.list_widget.count()):
            item: QListWidgetItem = self.list_widget.item(index)
示例#7
0
class FileSwitcher(QDialog):
    """A Sublime-like file switcher."""
    sig_goto_file = Signal(int, object)

    # Constants that define the mode in which the list widget is working
    # FILE_MODE is for a list of files, SYMBOL_MODE if for a list of symbols
    # in a given file when using the '@' symbol.
    FILE_MODE, SYMBOL_MODE = [1, 2]
    MAX_WIDTH = 600

    def __init__(self, parent, plugin, tabs, data, icon):
        QDialog.__init__(self, parent)

        # Variables
        self.plugins_tabs = []
        self.plugins_data = []
        self.plugins_instances = []
        self.add_plugin(plugin, tabs, data, icon)
        self.plugin = None                # Last plugin with focus
        self.mode = self.FILE_MODE        # By default start in this mode
        self.initial_cursors = None       # {fullpath: QCursor}
        self.initial_path = None          # Fullpath of initial active editor
        self.initial_widget = None        # Initial active editor
        self.line_number = None           # Selected line number in filer
        self.is_visible = False           # Is the switcher visible?

        help_text = _("Press <b>Enter</b> to switch files or <b>Esc</b> to "
                      "cancel.<br><br>Type to filter filenames.<br><br>"
                      "Use <b>:number</b> to go to a line, e.g. "
                      "<b><code>main:42</code></b><br>"
                      "Use <b>@symbol_text</b> to go to a symbol, e.g. "
                      "<b><code>@init</code></b>"
                      "<br><br> Press <b>Ctrl+W</b> to close current tab.<br>")

        # Either allow searching for a line number or a symbol but not both
        regex = QRegExp("([A-Za-z0-9_]{0,100}@[A-Za-z0-9_]{0,100})|" +
                        "([A-Za-z0-9_]{0,100}:{0,1}[0-9]{0,100})")

        # Widgets
        self.edit = FilesFilterLine(self)
        self.help = HelperToolButton()
        self.list = QListWidget(self)
        self.filter = KeyPressFilter()
        regex_validator = QRegExpValidator(regex, self.edit)

        # Widgets setup
        self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint)
        self.setWindowOpacity(0.95)
        self.edit.installEventFilter(self.filter)
        self.edit.setValidator(regex_validator)
        self.help.setToolTip(help_text)
        self.list.setItemDelegate(HTMLDelegate(self))

        # Layout
        edit_layout = QHBoxLayout()
        edit_layout.addWidget(self.edit)
        edit_layout.addWidget(self.help)
        layout = QVBoxLayout()
        layout.addLayout(edit_layout)
        layout.addWidget(self.list)
        self.setLayout(layout)

        # Signals
        self.rejected.connect(self.restore_initial_state)
        self.filter.sig_up_key_pressed.connect(self.previous_row)
        self.filter.sig_down_key_pressed.connect(self.next_row)
        self.edit.returnPressed.connect(self.accept)
        self.edit.textChanged.connect(self.setup)
        self.list.itemSelectionChanged.connect(self.item_selection_changed)
        self.list.clicked.connect(self.edit.setFocus)

    # --- Properties
    @property
    def widgets(self):
        widgets = []
        for tabs, plugin in self.plugins_tabs:
            widgets += [(tabs.widget(index), plugin) for
                        index in range(tabs.count())]
        return widgets

    @property
    def line_count(self):
        line_count = []
        for widget in self.widgets:
            try:
                current_line_count = widget[0].get_line_count()
            except AttributeError:
                current_line_count = 0
            line_count.append(current_line_count)
        return line_count

    @property
    def save_status(self):
        save_status = []
        for da, icon in self.plugins_data:
            save_status += [getattr(td, 'newly_created', False) for td in da]
        return save_status

    @property
    def paths(self):
        paths = []
        for da, icon in self.plugins_data:
            paths += [getattr(td, 'filename', None) for td in da]
        return paths

    @property
    def filenames(self):
        filenames = []
        for da, icon in self.plugins_data:
            filenames += [os.path.basename(getattr(td,
                                                   'filename',
                                                   None)) for td in da]
        return filenames

    @property
    def icons(self):
        icons = []
        for da, icon in self.plugins_data:
            icons += [icon for td in da]
        return icons

    @property
    def current_path(self):
        return self.paths_by_widget[self.get_widget()]

    @property
    def paths_by_widget(self):
        widgets = [w[0] for w in self.widgets]
        return dict(zip(widgets, self.paths))

    @property
    def widgets_by_path(self):
        widgets = [w[0] for w in self.widgets]
        return dict(zip(self.paths, widgets))

    @property
    def filter_text(self):
        """Get the normalized (lowecase) content of the filter text."""
        return to_text_string(self.edit.text()).lower()

    def set_search_text(self, _str):
        self.edit.setText(_str)

    def save_initial_state(self):
        """Save initial cursors and initial active widget."""
        paths = self.paths
        self.initial_widget = self.get_widget()
        self.initial_cursors = {}

        for i, editor in enumerate(self.widgets):
            if editor is self.initial_widget:
                self.initial_path = paths[i]
            # This try is needed to make the fileswitcher work with 
            # plugins that does not have a textCursor.
            try:
                self.initial_cursors[paths[i]] = editor.textCursor()
            except AttributeError:
                pass

    def accept(self):
        self.is_visible = False
        QDialog.accept(self)
        self.list.clear()

    def restore_initial_state(self):
        """Restores initial cursors and initial active editor."""
        self.list.clear()
        self.is_visible = False
        widgets = self.widgets_by_path

        if not self.edit.clicked_outside:
            for path in self.initial_cursors:
                cursor = self.initial_cursors[path]
                if path in widgets:
                    self.set_editor_cursor(widgets[path], cursor)

            if self.initial_widget in self.paths_by_widget:
                index = self.paths.index(self.initial_path)
                self.sig_goto_file.emit(index)

    def set_dialog_position(self):
        """Positions the file switcher dialog."""
        parent = self.parent()
        geo = parent.geometry()
        width = self.list.width()  # This has been set in setup
        left = parent.geometry().width()/2 - width/2
        # Note: the +1 pixel on the top makes it look better
        if isinstance(parent, QMainWindow):
            top = (parent.toolbars_menu.geometry().height() +
                   parent.menuBar().geometry().height() + 1)
        else:
            top = self.plugins_tabs[0][0].tabBar().geometry().height() + 1

        while parent:
            geo = parent.geometry()
            top += geo.top()
            left += geo.left()
            parent = parent.parent()

        self.move(left, top)

    def get_item_size(self, content):
        """
        Get the max size (width and height) for the elements of a list of
        strings as a QLabel.
        """
        strings = []
        if content:
            for rich_text in content:
                label = QLabel(rich_text)
                label.setTextFormat(Qt.PlainText)
                strings.append(label.text())
                fm = label.fontMetrics()

            return (max([fm.width(s) * 1.3 for s in strings]), fm.height())

    def fix_size(self, content):
        """
        Adjusts the width and height of the file switcher
        based on the relative size of the parent and content.
        """
        # Update size of dialog based on relative size of the parent
        if content:
            width, height = self.get_item_size(content)

            # Width
            parent = self.parent()
            relative_width = parent.geometry().width() * 0.65
            if relative_width > self.MAX_WIDTH:
                relative_width = self.MAX_WIDTH
            self.list.setMinimumWidth(relative_width)

            # Height
            if len(content) < 8:
                max_entries = len(content)
            else:
                max_entries = 8
            max_height = height * max_entries * 2.5
            self.list.setMinimumHeight(max_height)

            # Resize
            self.list.resize(relative_width, self.list.height())

    # --- Helper methods: List widget
    def count(self):
        """Gets the item count in the list widget."""
        return self.list.count()

    def current_row(self):
        """Returns the current selected row in the list widget."""
        return self.list.currentRow()

    def set_current_row(self, row):
        """Sets the current selected row in the list widget."""
        return self.list.setCurrentRow(row)

    def select_row(self, steps):
        """Select row in list widget based on a number of steps with direction.

        Steps can be positive (next rows) or negative (previous rows).
        """
        row = self.current_row() + steps
        if 0 <= row < self.count():
            self.set_current_row(row)

    def previous_row(self):
        """Select previous row in list widget."""
        if self.mode == self.SYMBOL_MODE:
            self.select_row(-1)
            return
        prev_row = self.current_row() - 1
        if prev_row >= 0:
            title = self.list.item(prev_row).text()
        else:
            title = ''
        if prev_row == 0 and '</b></big><br>' in title:
            self.list.scrollToTop()
        elif '</b></big><br>' in title:
            # Select the next previous row, the one following is a title
            self.select_row(-2)
        else:
            self.select_row(-1)

    def next_row(self):
        """Select next row in list widget."""
        if self.mode == self.SYMBOL_MODE:
            self.select_row(+1)
            return
        next_row = self.current_row() + 1
        if next_row < self.count():
            if '</b></big><br>' in self.list.item(next_row).text():
                # Select the next next row, the one following is a title
                self.select_row(+2)
            else:
                self.select_row(+1)

    def get_stack_index(self, stack_index, plugin_index):
        """Get the real index of the selected item."""
        other_plugins_count = sum([other_tabs[0].count() \
                                   for other_tabs in \
                                   self.plugins_tabs[:plugin_index]])
        real_index = stack_index - other_plugins_count

        return real_index

    # --- Helper methods: Widget
    def get_widget(self, index=None, path=None, tabs=None):
        """Get widget by index.
        
        If no tabs and index specified the current active widget is returned.
        """
        if index and tabs:
            return tabs.widget(index)
        elif path and tabs:
            return tabs.widget(index)
        elif self.plugin:
            index = self.plugins_instances.index(self.plugin)
            return self.plugins_tabs[index][0].currentWidget()
        else:
            return self.plugins_tabs[0][0].currentWidget()

    def set_editor_cursor(self, editor, cursor):
        """Set the cursor of an editor."""
        pos = cursor.position()
        anchor = cursor.anchor()

        new_cursor = QTextCursor()
        if pos == anchor:
            new_cursor.movePosition(pos)
        else:
            new_cursor.movePosition(anchor)
            new_cursor.movePosition(pos, QTextCursor.KeepAnchor)
        editor.setTextCursor(cursor)

    def goto_line(self, line_number):
        """Go to specified line number in current active editor."""
        if line_number:
            line_number = int(line_number)
            editor = self.get_widget()
            try:
                editor.go_to_line(min(line_number, editor.get_line_count()))
            except AttributeError:
                pass

    # --- Helper methods: Outline explorer
    def get_symbol_list(self):
        """Get the list of symbols present in the file."""
        try:
            oedata = self.get_widget().get_outlineexplorer_data()
        except AttributeError:
            oedata = {}
        return oedata

    # --- Handlers
    def item_selection_changed(self):
        """List widget item selection change handler."""
        row = self.current_row()
        if self.count() and row >= 0:
            if '</b></big><br>' in self.list.currentItem().text() and row == 0:
                self.next_row()
            if self.mode == self.FILE_MODE:
                try:
                    stack_index = self.paths.index(self.filtered_path[row])
                    self.plugin = self.widgets[stack_index][1]
                    plugin_index = self.plugins_instances.index(self.plugin)
                    # Count the real index in the tabWidget of the
                    # current plugin
                    real_index = self.get_stack_index(stack_index,
                                                      plugin_index)
                    self.sig_goto_file.emit(real_index,
                                            self.plugin.get_current_tab_manager())
                    self.goto_line(self.line_number)
                    try:
                        self.plugin.switch_to_plugin()
                        self.raise_()
                    except AttributeError:
                        # The widget using the fileswitcher is not a plugin
                        pass
                    self.edit.setFocus()
                except ValueError:
                    pass
            else:
                line_number = self.filtered_symbol_lines[row]
                self.goto_line(line_number)

    def setup_file_list(self, filter_text, current_path):
        """Setup list widget content for file list display."""
        short_paths = shorten_paths(self.paths, self.save_status)
        paths = self.paths
        icons = self.icons
        results = []
        trying_for_line_number = ':' in filter_text

        # Get optional line number
        if trying_for_line_number:
            filter_text, line_number = filter_text.split(':')
            # Get all the available filenames
            scores = get_search_scores('', self.filenames,
                                       template="<b>{0}</b>")
        else:
            line_number = None
            # Get all available filenames and get the scores for
            # "fuzzy" matching
            scores = get_search_scores(filter_text, self.filenames,
                                       template="<b>{0}</b>")


        # Get max width to determine if shortpaths should be used
        max_width = self.get_item_size(paths)[0]
        self.fix_size(paths)

        # Build the text that will appear on the list widget
        for index, score in enumerate(scores):
            text, rich_text, score_value = score
            if score_value != -1:
                text_item = '<big>' + rich_text.replace('&', '') + '</big>'
                if trying_for_line_number:
                    text_item += " [{0:} {1:}]".format(self.line_count[index],
                                                       _("lines"))
                if max_width > self.list.width():
                    text_item += u"<br><i>{0:}</i>".format(short_paths[index])
                else:
                    text_item += u"<br><i>{0:}</i>".format(paths[index])
                if (trying_for_line_number and self.line_count[index] != 0 or
                        not trying_for_line_number):
                    results.append((score_value, index, text_item))

        # Sort the obtained scores and populate the list widget
        self.filtered_path = []
        plugin = None
        for result in sorted(results):
            index = result[1]
            path = paths[index]
            icon = icons[index]
            text = ''
            try:
                title = self.widgets[index][1].get_plugin_title().split(' - ')
                if plugin != title[0]:
                    plugin = title[0]
                    text += '<br><big><b>' + plugin + '</b></big><br>'
                    item = QListWidgetItem(text)
                    item.setToolTip(path)
                    item.setSizeHint(QSize(0, 25))
                    item.setFlags(Qt.ItemIsEditable)
                    self.list.addItem(item)
                    self.filtered_path.append(path)
            except:
                # The widget using the fileswitcher is not a plugin
                pass
            text = ''
            text += result[-1]
            item = QListWidgetItem(icon, text)
            item.setToolTip(path)
            item.setSizeHint(QSize(0, 25))
            self.list.addItem(item)
            self.filtered_path.append(path)

        # To adjust the delegate layout for KDE themes
        self.list.files_list = True

        # Move selected item in list accordingly and update list size
        if current_path in self.filtered_path:
            self.set_current_row(self.filtered_path.index(current_path))
        elif self.filtered_path:
            self.set_current_row(0)

        # If a line number is searched look for it
        self.line_number = line_number
        self.goto_line(line_number)

    def setup_symbol_list(self, filter_text, current_path):
        """Setup list widget content for symbol list display."""
        # Get optional symbol name
        filter_text, symbol_text = filter_text.split('@')

        # Fetch the Outline explorer data, get the icons and values
        oedata = self.get_symbol_list()
        icons = get_python_symbol_icons(oedata)

        # The list of paths here is needed in order to have the same
        # point of measurement for the list widget size as in the file list
        # See issue 4648
        paths = self.paths
        # Update list size
        self.fix_size(paths)

        symbol_list = process_python_symbol_data(oedata)
        line_fold_token = [(item[0], item[2], item[3]) for item in symbol_list]
        choices = [item[1] for item in symbol_list]
        scores = get_search_scores(symbol_text, choices, template="<b>{0}</b>")

        # Build the text that will appear on the list widget
        results = []
        lines = []
        self.filtered_symbol_lines = []
        for index, score in enumerate(scores):
            text, rich_text, score_value = score
            line, fold_level, token = line_fold_token[index]
            lines.append(text)
            if score_value != -1:
                results.append((score_value, line, text, rich_text,
                                fold_level, icons[index], token))

        template = '{0}{1}'

        for (score, line, text, rich_text, fold_level, icon,
             token) in sorted(results):
            fold_space = '&nbsp;'*(fold_level)
            line_number = line + 1
            self.filtered_symbol_lines.append(line_number)
            textline = template.format(fold_space, rich_text)
            item = QListWidgetItem(icon, textline)
            item.setSizeHint(QSize(0, 16))
            self.list.addItem(item)

        # To adjust the delegate layout for KDE themes
        self.list.files_list = False

        # Move selected item in list accordingly
        # NOTE: Doing this is causing two problems:
        # 1. It makes the cursor to auto-jump to the last selected
        #    symbol after opening or closing a different file
        # 2. It moves the cursor to the first symbol by default,
        #    which is very distracting.
        # That's why this line is commented!
        # self.set_current_row(0)

    def setup(self):
        """Setup list widget content."""
        if len(self.plugins_tabs) == 0:
            self.close()
            return

        self.list.clear()
        current_path = self.current_path
        filter_text = self.filter_text

        # Get optional line or symbol to define mode and method handler
        trying_for_symbol = ('@' in self.filter_text)

        if trying_for_symbol:
            self.mode = self.SYMBOL_MODE
            self.setup_symbol_list(filter_text, current_path)
        else:
            self.mode = self.FILE_MODE
            self.setup_file_list(filter_text, current_path)

        # Set position according to size
        self.set_dialog_position()

    def add_plugin(self, plugin, tabs, data, icon):
        """Add a plugin to display its files."""
        self.plugins_tabs.append((tabs, plugin))
        self.plugins_data.append((data, icon))
        self.plugins_instances.append(plugin)
示例#8
0
class FileSwitcher(QDialog):
    """A Sublime-like file switcher."""
    sig_goto_file = Signal(int, object)

    # Constants that define the mode in which the list widget is working
    # FILE_MODE is for a list of files, SYMBOL_MODE if for a list of symbols
    # in a given file when using the '@' symbol.
    FILE_MODE, SYMBOL_MODE = [1, 2]
    MAX_WIDTH = 600

    def __init__(self, parent, plugin, tabs, data, icon):
        QDialog.__init__(self, parent)

        # Variables
        self.plugins_tabs = []
        self.plugins_data = []
        self.plugins_instances = []
        self.add_plugin(plugin, tabs, data, icon)
        self.plugin = None  # Last plugin with focus
        self.mode = self.FILE_MODE  # By default start in this mode
        self.initial_cursors = None  # {fullpath: QCursor}
        self.initial_path = None  # Fullpath of initial active editor
        self.initial_widget = None  # Initial active editor
        self.line_number = None  # Selected line number in filer
        self.is_visible = False  # Is the switcher visible?

        help_text = _("Press <b>Enter</b> to switch files or <b>Esc</b> to "
                      "cancel.<br><br>Type to filter filenames.<br><br>"
                      "Use <b>:number</b> to go to a line, e.g. "
                      "<b><code>main:42</code></b><br>"
                      "Use <b>@symbol_text</b> to go to a symbol, e.g. "
                      "<b><code>@init</code></b>"
                      "<br><br> Press <b>Ctrl+W</b> to close current tab.<br>")

        # Either allow searching for a line number or a symbol but not both
        regex = QRegExp("([A-Za-z0-9_]{0,100}@[A-Za-z0-9_]{0,100})|" +
                        "([A-Za-z0-9_]{0,100}:{0,1}[0-9]{0,100})")

        # Widgets
        self.edit = FilesFilterLine(self)
        self.help = HelperToolButton()
        self.list = QListWidget(self)
        self.filter = KeyPressFilter()
        regex_validator = QRegExpValidator(regex, self.edit)

        # Widgets setup
        self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint)
        self.setWindowOpacity(0.95)
        self.edit.installEventFilter(self.filter)
        self.edit.setValidator(regex_validator)
        self.help.setToolTip(help_text)
        self.list.setItemDelegate(HTMLDelegate(self))

        # Layout
        edit_layout = QHBoxLayout()
        edit_layout.addWidget(self.edit)
        edit_layout.addWidget(self.help)
        layout = QVBoxLayout()
        layout.addLayout(edit_layout)
        layout.addWidget(self.list)
        self.setLayout(layout)

        # Signals
        self.rejected.connect(self.restore_initial_state)
        self.filter.sig_up_key_pressed.connect(self.previous_row)
        self.filter.sig_down_key_pressed.connect(self.next_row)
        self.edit.returnPressed.connect(self.accept)
        self.edit.textChanged.connect(self.setup)
        self.list.itemSelectionChanged.connect(self.item_selection_changed)
        self.list.clicked.connect(self.edit.setFocus)

    # --- Properties
    @property
    def widgets(self):
        widgets = []
        for plugin in self.plugins_instances:
            tabs = self.get_plugin_tabwidget(plugin)
            widgets += [(tabs.widget(index), plugin)
                        for index in range(tabs.count())]
        return widgets

    @property
    def line_count(self):
        line_count = []
        for widget in self.widgets:
            try:
                current_line_count = widget[0].get_line_count()
            except AttributeError:
                current_line_count = 0
            line_count.append(current_line_count)
        return line_count

    @property
    def save_status(self):
        save_status = []
        for da, icon in self.plugins_data:
            save_status += [getattr(td, 'newly_created', False) for td in da]
        return save_status

    @property
    def paths(self):
        paths = []
        for plugin in self.plugins_instances:
            da = self.get_plugin_data(plugin)
            paths += [getattr(td, 'filename', None) for td in da]
        return paths

    @property
    def filenames(self):
        filenames = []
        for plugin in self.plugins_instances:
            da = self.get_plugin_data(plugin)
            filenames += [
                os.path.basename(getattr(td, 'filename', None)) for td in da
            ]
        return filenames

    @property
    def icons(self):
        icons = []
        for da, icon in self.plugins_data:
            icons += [icon for td in da]
        return icons

    @property
    def current_path(self):
        return self.paths_by_widget[self.get_widget()]

    @property
    def paths_by_widget(self):
        widgets = [w[0] for w in self.widgets]
        return dict(zip(widgets, self.paths))

    @property
    def widgets_by_path(self):
        widgets = [w[0] for w in self.widgets]
        return dict(zip(self.paths, widgets))

    @property
    def filter_text(self):
        """Get the normalized (lowecase) content of the filter text."""
        return to_text_string(self.edit.text()).lower()

    def set_search_text(self, _str):
        self.edit.setText(_str)

    def save_initial_state(self):
        """Save initial cursors and initial active widget."""
        paths = self.paths
        self.initial_widget = self.get_widget()
        self.initial_cursors = {}

        for i, editor in enumerate(self.widgets):
            if editor is self.initial_widget:
                self.initial_path = paths[i]
            # This try is needed to make the fileswitcher work with
            # plugins that does not have a textCursor.
            try:
                self.initial_cursors[paths[i]] = editor.textCursor()
            except AttributeError:
                pass

    def accept(self):
        self.is_visible = False
        QDialog.accept(self)
        self.list.clear()

    def restore_initial_state(self):
        """Restores initial cursors and initial active editor."""
        self.list.clear()
        self.is_visible = False
        widgets = self.widgets_by_path

        if not self.edit.clicked_outside:
            for path in self.initial_cursors:
                cursor = self.initial_cursors[path]
                if path in widgets:
                    self.set_editor_cursor(widgets[path], cursor)

            if self.initial_widget in self.paths_by_widget:
                index = self.paths.index(self.initial_path)
                self.sig_goto_file.emit(index)

    def set_dialog_position(self):
        """Positions the file switcher dialog."""
        parent = self.parent()
        geo = parent.geometry()
        width = self.list.width()  # This has been set in setup
        left = parent.geometry().width() / 2 - width / 2
        # Note: the +1 pixel on the top makes it look better
        if isinstance(parent, QMainWindow):
            top = (parent.toolbars_menu.geometry().height() +
                   parent.menuBar().geometry().height() + 1)
        else:
            top = self.plugins_tabs[0][0].tabBar().geometry().height() + 1

        while parent:
            geo = parent.geometry()
            top += geo.top()
            left += geo.left()
            parent = parent.parent()

        self.move(left, top)

    def get_item_size(self, content):
        """
        Get the max size (width and height) for the elements of a list of
        strings as a QLabel.
        """
        strings = []
        if content:
            for rich_text in content:
                label = QLabel(rich_text)
                label.setTextFormat(Qt.PlainText)
                strings.append(label.text())
                fm = label.fontMetrics()

            return (max([fm.width(s) * 1.3 for s in strings]), fm.height())

    def fix_size(self, content):
        """
        Adjusts the width and height of the file switcher
        based on the relative size of the parent and content.
        """
        # Update size of dialog based on relative size of the parent
        if content:
            width, height = self.get_item_size(content)

            # Width
            parent = self.parent()
            relative_width = parent.geometry().width() * 0.65
            if relative_width > self.MAX_WIDTH:
                relative_width = self.MAX_WIDTH
            self.list.setMinimumWidth(relative_width)

            # Height
            if len(content) < 8:
                max_entries = len(content)
            else:
                max_entries = 8
            max_height = height * max_entries * 2.5
            self.list.setMinimumHeight(max_height)

            # Resize
            self.list.resize(relative_width, self.list.height())

    # --- Helper methods: List widget
    def count(self):
        """Gets the item count in the list widget."""
        return self.list.count()

    def current_row(self):
        """Returns the current selected row in the list widget."""
        return self.list.currentRow()

    def set_current_row(self, row):
        """Sets the current selected row in the list widget."""
        return self.list.setCurrentRow(row)

    def select_row(self, steps):
        """Select row in list widget based on a number of steps with direction.

        Steps can be positive (next rows) or negative (previous rows).
        """
        row = self.current_row() + steps
        if 0 <= row < self.count():
            self.set_current_row(row)

    def previous_row(self):
        """Select previous row in list widget."""
        if self.mode == self.SYMBOL_MODE:
            self.select_row(-1)
            return
        prev_row = self.current_row() - 1
        if prev_row >= 0:
            title = self.list.item(prev_row).text()
        else:
            title = ''
        if prev_row == 0 and '</b></big><br>' in title:
            self.list.scrollToTop()
        elif '</b></big><br>' in title:
            # Select the next previous row, the one following is a title
            self.select_row(-2)
        else:
            self.select_row(-1)

    def next_row(self):
        """Select next row in list widget."""
        if self.mode == self.SYMBOL_MODE:
            self.select_row(+1)
            return
        next_row = self.current_row() + 1
        if next_row < self.count():
            if '</b></big><br>' in self.list.item(next_row).text():
                # Select the next next row, the one following is a title
                self.select_row(+2)
            else:
                self.select_row(+1)

    def get_stack_index(self, stack_index, plugin_index):
        """Get the real index of the selected item."""
        other_plugins_count = sum([other_tabs[0].count() \
                                   for other_tabs in \
                                   self.plugins_tabs[:plugin_index]])
        real_index = stack_index - other_plugins_count

        return real_index

    # --- Helper methods: Widget
    def get_plugin_data(self, plugin):
        """Get the data object of the plugin's current tab manager."""
        # The data object is named "data" in the editor plugin while it is
        # named "clients" in the notebook plugin.
        try:
            data = plugin.get_current_tab_manager().data
        except AttributeError:
            data = plugin.get_current_tab_manager().clients

        return data

    def get_plugin_tabwidget(self, plugin):
        """Get the tabwidget of the plugin's current tab manager."""
        # The tab widget is named "tabs" in the editor plugin while it is
        # named "tabwidget" in the notebook plugin.
        try:
            tabwidget = plugin.get_current_tab_manager().tabs
        except AttributeError:
            tabwidget = plugin.get_current_tab_manager().tabwidget

        return tabwidget

    def get_widget(self, index=None, path=None, tabs=None):
        """Get widget by index.

        If no tabs and index specified the current active widget is returned.
        """
        if (index and tabs) or (path and tabs):
            return tabs.widget(index)
        elif self.plugin:
            return self.get_plugin_tabwidget(self.plugin).currentWidget()
        else:
            return self.plugins_tabs[0][0].currentWidget()

    def set_editor_cursor(self, editor, cursor):
        """Set the cursor of an editor."""
        pos = cursor.position()
        anchor = cursor.anchor()

        new_cursor = QTextCursor()
        if pos == anchor:
            new_cursor.movePosition(pos)
        else:
            new_cursor.movePosition(anchor)
            new_cursor.movePosition(pos, QTextCursor.KeepAnchor)
        editor.setTextCursor(cursor)

    def goto_line(self, line_number):
        """Go to specified line number in current active editor."""
        if line_number:
            line_number = int(line_number)
            try:
                self.plugin.go_to_line(line_number)
            except AttributeError:
                pass

    # --- Helper methods: Outline explorer
    def get_symbol_list(self):
        """Get the list of symbols present in the file."""
        try:
            oedata = self.get_widget().get_outlineexplorer_data()
        except AttributeError:
            oedata = {}
        return oedata

    # --- Handlers
    def item_selection_changed(self):
        """List widget item selection change handler."""
        row = self.current_row()
        if self.count() and row >= 0:
            if '</b></big><br>' in self.list.currentItem().text() and row == 0:
                self.next_row()
            if self.mode == self.FILE_MODE:
                try:
                    stack_index = self.paths.index(self.filtered_path[row])
                    self.plugin = self.widgets[stack_index][1]
                    plugin_index = self.plugins_instances.index(self.plugin)
                    # Count the real index in the tabWidget of the
                    # current plugin
                    real_index = self.get_stack_index(stack_index,
                                                      plugin_index)
                    self.sig_goto_file.emit(
                        real_index, self.plugin.get_current_tab_manager())
                    self.goto_line(self.line_number)
                    try:
                        self.plugin.switch_to_plugin()
                        self.raise_()
                    except AttributeError:
                        # The widget using the fileswitcher is not a plugin
                        pass
                    self.edit.setFocus()
                except ValueError:
                    pass
            else:
                line_number = self.filtered_symbol_lines[row]
                self.goto_line(line_number)

    def setup_file_list(self, filter_text, current_path):
        """Setup list widget content for file list display."""
        short_paths = shorten_paths(self.paths, self.save_status)
        paths = self.paths
        icons = self.icons
        results = []
        trying_for_line_number = ':' in filter_text

        # Get optional line number
        if trying_for_line_number:
            filter_text, line_number = filter_text.split(':')
            if line_number == '':
                line_number = None
            # Get all the available filenames
            scores = get_search_scores('',
                                       self.filenames,
                                       template="<b>{0}</b>")
        else:
            line_number = None
            # Get all available filenames and get the scores for
            # "fuzzy" matching
            scores = get_search_scores(filter_text,
                                       self.filenames,
                                       template="<b>{0}</b>")

        # Get max width to determine if shortpaths should be used
        max_width = self.get_item_size(paths)[0]
        self.fix_size(paths)

        # Build the text that will appear on the list widget
        for index, score in enumerate(scores):
            text, rich_text, score_value = score
            if score_value != -1:
                text_item = '<big>' + rich_text.replace('&', '') + '</big>'
                if trying_for_line_number:
                    text_item += " [{0:} {1:}]".format(self.line_count[index],
                                                       _("lines"))
                if max_width > self.list.width():
                    text_item += u"<br><i>{0:}</i>".format(short_paths[index])
                else:
                    text_item += u"<br><i>{0:}</i>".format(paths[index])
                if (trying_for_line_number and self.line_count[index] != 0
                        or not trying_for_line_number):
                    results.append((score_value, index, text_item))

        # Sort the obtained scores and populate the list widget
        self.filtered_path = []
        plugin = None
        for result in sorted(results):
            index = result[1]
            path = paths[index]
            icon = icons[index]
            text = ''
            try:
                title = self.widgets[index][1].get_plugin_title().split(' - ')
                if plugin != title[0]:
                    plugin = title[0]
                    text += '<br><big><b>' + plugin + '</b></big><br>'
                    item = QListWidgetItem(text)
                    item.setToolTip(path)
                    item.setSizeHint(QSize(0, 25))
                    item.setFlags(Qt.ItemIsEditable)
                    self.list.addItem(item)
                    self.filtered_path.append(path)
            except:
                # The widget using the fileswitcher is not a plugin
                pass
            text = ''
            text += result[-1]
            item = QListWidgetItem(icon, text)
            item.setToolTip(path)
            item.setSizeHint(QSize(0, 25))
            self.list.addItem(item)
            self.filtered_path.append(path)

        # To adjust the delegate layout for KDE themes
        self.list.files_list = True

        # Move selected item in list accordingly and update list size
        if current_path in self.filtered_path:
            self.set_current_row(self.filtered_path.index(current_path))
        elif self.filtered_path:
            self.set_current_row(0)

        # If a line number is searched look for it
        self.line_number = line_number
        self.goto_line(line_number)

    def setup_symbol_list(self, filter_text, current_path):
        """Setup list widget content for symbol list display."""
        # Get optional symbol name
        filter_text, symbol_text = filter_text.split('@')

        # Fetch the Outline explorer data, get the icons and values
        oedata = self.get_symbol_list()
        icons = get_python_symbol_icons(oedata)

        # The list of paths here is needed in order to have the same
        # point of measurement for the list widget size as in the file list
        # See issue 4648
        paths = self.paths
        # Update list size
        self.fix_size(paths)

        symbol_list = process_python_symbol_data(oedata)
        line_fold_token = [(item[0], item[2], item[3]) for item in symbol_list]
        choices = [item[1] for item in symbol_list]
        scores = get_search_scores(symbol_text, choices, template="<b>{0}</b>")

        # Build the text that will appear on the list widget
        results = []
        lines = []
        self.filtered_symbol_lines = []
        for index, score in enumerate(scores):
            text, rich_text, score_value = score
            line, fold_level, token = line_fold_token[index]
            lines.append(text)
            if score_value != -1:
                results.append((score_value, line, text, rich_text, fold_level,
                                icons[index], token))

        template = '{0}{1}'

        for (score, line, text, rich_text, fold_level, icon,
             token) in sorted(results):
            fold_space = '&nbsp;' * (fold_level)
            line_number = line + 1
            self.filtered_symbol_lines.append(line_number)
            textline = template.format(fold_space, rich_text)
            item = QListWidgetItem(icon, textline)
            item.setSizeHint(QSize(0, 16))
            self.list.addItem(item)

        # To adjust the delegate layout for KDE themes
        self.list.files_list = False

        # Select edit line when using symbol search initially.
        # See issue 5661
        self.edit.setFocus()

        # Move selected item in list accordingly
        # NOTE: Doing this is causing two problems:
        # 1. It makes the cursor to auto-jump to the last selected
        #    symbol after opening or closing a different file
        # 2. It moves the cursor to the first symbol by default,
        #    which is very distracting.
        # That's why this line is commented!
        # self.set_current_row(0)

    def setup(self):
        """Setup list widget content."""
        if len(self.plugins_tabs) == 0:
            self.close()
            return

        self.list.clear()
        current_path = self.current_path
        filter_text = self.filter_text

        # Get optional line or symbol to define mode and method handler
        trying_for_symbol = ('@' in self.filter_text)

        if trying_for_symbol:
            self.mode = self.SYMBOL_MODE
            self.setup_symbol_list(filter_text, current_path)
        else:
            self.mode = self.FILE_MODE
            self.setup_file_list(filter_text, current_path)

        # Set position according to size
        self.set_dialog_position()

    def show(self):
        """
        Override Qt method to force an update of the fileswitcher before
        showing it. See Issue #5317 and PR #5389.
        """
        self.setup()
        super(FileSwitcher, self).show()

    def add_plugin(self, plugin, tabs, data, icon):
        """Add a plugin to display its files."""
        self.plugins_tabs.append((tabs, plugin))
        self.plugins_data.append((data, icon))
        self.plugins_instances.append(plugin)
示例#9
0
class EditableList(QWidget):
    """
    Editable list with label, add, delete and edit buttons.
    """
    sig_item_edited = Signal(object)
    sig_item_removed = Signal(object)
    sig_item_selected = Signal()

    def __init__(self,
                 title=None,
                 items_text=[],
                 duplicates=False,
                 normalize=True,
                 min_items=1,
                 confirm_remove=True,
                 regex=None,
                 tooltip=None):
        super(EditableList, self).__init__()

        self.duplicates = duplicates
        self.items_text = items_text
        self.normalize = normalize
        self.min_items = min_items
        self.confirm_remove = confirm_remove

        # Widgets
        self.label_title = QLabel(title)
        self.button_add = QPushButton(qta.icon('fa.plus'), '')
        self.button_remove = QPushButton(qta.icon('fa.minus'), '')
        self.button_edit = QPushButton(qta.icon('fa.edit'), '')
        self.list = QListWidget()
        self.delegate = EditableListDelegate(regex=regex, tooltip=tooltip)

        # Widget setup
        self.list.setItemDelegate(self.delegate)
        self.setMaximumHeight(self._height() * 4)

        # Layout
        label_buttons_layout = QHBoxLayout()
        label_buttons_layout.addWidget(self.label_title, 0)
        label_buttons_layout.addStretch()
        label_buttons_layout.addWidget(self.button_add)
        label_buttons_layout.addWidget(self.button_remove)
        label_buttons_layout.addWidget(self.button_edit)

        layout = QVBoxLayout()
        layout.addLayout(label_buttons_layout)
        layout.addWidget(self.list)

        self.setLayout(layout)

        # Signals
        self.button_add.clicked.connect(self.add)
        self.button_edit.clicked.connect(self.edit)
        self.button_remove.clicked.connect(self.remove)

        self.delegate.closeEditor.connect(self.check_value)
        self.list.currentItemChanged.connect(self.refresh)
        self.list.itemSelectionChanged.connect(self.sig_item_selected)

        # Setup
        self.setup()

        # Expose list methods
        self.clear = self.list.clear
        self.currentItem = self.list.currentItem
        self.setCurrentRow = self.list.setCurrentRow
        self.currentRow = self.list.currentRow
        self.item = self.list.item
        self.count = self.list.count

    def _height(self):
        """
        Get the height for the row in the widget based on OS font metrics.
        """
        return self.fontMetrics().height() * 2

    def setup(self):
        """
        Initial setup for populating items if any.
        """
        # TODO: Check against regex and raise error accordingly!
        new_items = []
        for text in self.items_text:
            if self.normalize:
                text = text.lower()
            new_items.append(text)

        self.items_text = new_items

        if not self.duplicates:
            if len(set(self.items_text)) != len(self.items_text):
                raise Exception('The list cannot contains duplicates.')

        for item in self.items_text:
            item = QListWidgetItem(item)
            item.extra_data = None
            item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
            self.add(item)
            item.setSizeHint(QSize(item.sizeHint().width(), self._height()))

        self.refresh()

    def get_texts(self):
        """
        Returns the list of texts in the list, excluding the ones under
        edition.
        """
        row = self.list.currentRow()
        texts = []
        for i in range(self.list.count()):
            item = self.list.item(i)
            if item:
                text = item.text().lower() if self.normalize else item.text()
                texts.append(text)

        # Check for duplicates. But the entered text already is part of the
        # items, so that needs to be removed to make the check
        if texts and row != -1:
            texts.pop(row)

        return texts

    def add(self, item=None):
        """
        Return the text of all items in the list, except the current one being
        edited.
        """
        if item:
            if item.text() in self.get_texts() and not self.duplicates:
                raise Exception
            else:
                self.list.addItem(item)
        else:
            item = QListWidgetItem()
            item.extra_data = None
            item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable
                          | Qt.ItemIsEditable)
            self.list.addItem(item)
            self.list.setCurrentItem(item)
            item.setSizeHint(QSize(item.sizeHint().width(), self._height()))
            self.edit()
        self.refresh()

    def remove(self):
        """
        Remove item fron the list.
        """
        row = self.list.currentRow()
        item = self.list.currentItem()

        if item is None:
            self.refresh()
        else:
            text = item.text()

        if self.confirm_remove:
            message = ("Are you sure you want to remove<br>"
                       "<strong>{0}</strong>?".format(text))
            reply = QMessageBox.question(self, 'Remove item', message,
                                         QMessageBox.Yes, QMessageBox.No)
            remove_item = reply == QMessageBox.Yes
        else:
            remove_item = True

        if row != -1 and remove_item:
            item = self.list.takeItem(row)
            self.sig_item_removed.emit(item)

        self.refresh()

    def edit(self):
        """
        Start editing item from the list.
        """
        self.button_remove.setDisabled(True)
        self.button_add.setDisabled(True)
        self.button_edit.setDisabled(True)
        item = self.current_item()
        if item:
            item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable
                          | Qt.ItemIsEditable)
            item._text = item.text()
            self.list.editItem(item)

    def current_item(self):
        """
        Return the current selected item.
        """
        item = None
        row = self.list.currentRow()
        if row != -1:
            item = self.list.item(row)
        return item

    def check_value(self):
        """
        After editing an item check the value is valid and return to edit mode
        if it does not pass, or accept the value and add it.
        """
        texts = self.get_texts()
        item = self.current_item()
        self._temp_item = item
        if item:
            text = item.text().lower() if self.normalize else item.text()
            row = self.list.currentRow()
            if text.strip() == '':
                self.list.takeItem(row)
            if text.strip() in texts and not self.duplicates:
                self.edit()
            else:
                self.sig_item_edited.emit(item)
            item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)

    def refresh(self):
        """
        Refresh the enabled status of the actions buttons.
        """
        current_row = self.list.currentRow()

        if self.list.count() == 0:
            self.button_edit.setDisabled(True)
            self.button_remove.setDisabled(True)

        elif self.list.count() != 0 and current_row == -1:
            self.button_edit.setDisabled(True)
            self.button_remove.setDisabled(True)

        elif self.list.count() == self.min_items:
            self.button_add.setDisabled(False)
            self.button_edit.setDisabled(False)
            self.button_remove.setDisabled(True)

        elif self.list.count() != 0 and current_row != -1:
            self.button_add.setDisabled(False)
            self.button_edit.setDisabled(False)
            self.button_remove.setDisabled(False)

        for i in range(self.list.count()):
            item = self.list.item(i)
            item.setSizeHint(QSize(item.sizeHint().width(), self._height()))
示例#10
0
class PreferencesDialog(QDialog):
    """Preferences Dialog for Napari user settings."""

    ui_schema = {
        "call_order": {
            "ui:widget": "plugins"
        },
    }

    resized = Signal(QSize)
    closed = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)

        self._list = QListWidget(self)
        self._stack = QStackedWidget(self)

        self._list.setObjectName("Preferences")

        # Set up buttons
        self._button_cancel = QPushButton(trans._("Cancel"))
        self._button_ok = QPushButton(trans._("OK"))
        self._default_restore = QPushButton(trans._("Restore defaults"))

        # Setup
        self.setWindowTitle(trans._("Preferences"))

        # Layout
        left_layout = QVBoxLayout()
        left_layout.addWidget(self._list)
        left_layout.addStretch()
        left_layout.addWidget(self._default_restore)
        left_layout.addWidget(self._button_cancel)
        left_layout.addWidget(self._button_ok)

        main_layout = QHBoxLayout()
        main_layout.addLayout(left_layout, 1)
        main_layout.addWidget(self._stack, 3)

        self.setLayout(main_layout)

        # Signals

        self._list.currentRowChanged.connect(
            lambda index: self._stack.setCurrentIndex(index))
        self._button_cancel.clicked.connect(self.on_click_cancel)
        self._button_ok.clicked.connect(self.on_click_ok)
        self._default_restore.clicked.connect(self.restore_defaults)

        # Make widget

        self.make_dialog()
        self._list.setCurrentRow(0)

    def closeEvent(self, event):
        """Override to emit signal."""
        self.closed.emit()
        super().closeEvent(event)

    def reject(self):
        """Override to handle Escape."""
        super().reject()
        self.close()

    def resizeEvent(self, event):
        """Override to emit signal."""
        self.resized.emit(event.size())
        super().resizeEvent(event)

    def make_dialog(self):
        """Removes settings not to be exposed to user and creates dialog pages."""

        # Because there are multiple pages, need to keep a dictionary of values dicts.
        # One set of keywords are for each page, then in each entry for a page, there are dicts
        # of setting and its value.

        self._values_orig_dict = {}
        self._values_dict = {}
        self._setting_changed_dict = {}

        for page, setting in SETTINGS.schemas().items():
            schema, values, properties = self.get_page_dict(setting)

            self._setting_changed_dict[page] = {}
            self._values_orig_dict[page] = values
            self._values_dict[page] = values

            # Only add pages if there are any properties to add.
            if properties:
                self.add_page(schema, values)

    def get_page_dict(self, setting):
        """Provides the schema, set of values for each setting, and the properties
        for each setting.

        Parameters
        ----------
        setting : dict
            Dictionary of settings for a page within the settings manager.

        Returns
        -------
        schema : dict
            Json schema of the setting page.
        values : dict
            Dictionary of values currently set for each parameter in the settings.
        properties : dict
            Dictionary of properties within the json schema.

        """

        schema = json.loads(setting['json_schema'])
        # Need to remove certain properties that will not be displayed on the GUI
        properties = schema.pop('properties')
        model = setting['model']
        values = model.dict()
        napari_config = getattr(model, "NapariConfig", None)
        if napari_config is not None:
            for val in napari_config.preferences_exclude:
                properties.pop(val)
                values.pop(val)

        schema['properties'] = properties

        return schema, values, properties

    def restore_defaults(self):
        """Launches dialog to confirm restore settings choice."""

        widget = ConfirmDialog(
            parent=self,
            text=trans._("Are you sure you want to restore default settings?"),
        )
        widget.valueChanged.connect(self._reset_widgets)
        widget.exec_()

    def _reset_widgets(self):
        """Deletes the widgets and rebuilds with defaults."""
        self.close()
        self._list.clear()

        for n in range(self._stack.count()):
            widget = self._stack.removeWidget(self._stack.currentWidget())
            del widget

        self.make_dialog()
        self._list.setCurrentRow(0)
        self.show()

    def on_click_ok(self):
        """Keeps the selected preferences saved to SETTINGS."""
        self.close()

    def on_click_cancel(self):
        """Restores the settings in place when dialog was launched."""
        # Need to check differences for each page.
        for n in range(self._stack.count()):
            # Must set the current row so that the proper list is updated
            # in check differences.
            self._list.setCurrentRow(n)
            page = self._list.currentItem().text().split(" ")[0].lower()
            # get new values for settings.  If they were changed from values at beginning
            # of preference dialog session, change them back.
            # Using the settings value seems to be the best way to get the checkboxes right
            # on the plugin call order widget.
            setting = SETTINGS.schemas()[page]
            schema, new_values, properties = self.get_page_dict(setting)
            self.check_differences(self._values_orig_dict[page], new_values)

        self._list.setCurrentRow(0)
        self.close()

    def add_page(self, schema, values):
        """Creates a new page for each section in dialog.

        Parameters
        ----------
        schema : dict
            Json schema including all information to build each page in the
            preferences dialog.
        values : dict
            Dictionary of current values set in preferences.
        """
        widget = self.build_page_dialog(schema, values)
        self._list.addItem(schema["title"])
        self._stack.addWidget(widget)

    def build_page_dialog(self, schema, values):
        """Builds the preferences widget using the json schema builder.

        Parameters
        ----------
        schema : dict
            Json schema including all information to build each page in the
            preferences dialog.
        values : dict
            Dictionary of current values set in preferences.
        """

        builder = WidgetBuilder()
        form = builder.create_form(schema, self.ui_schema)
        # set state values for widget
        form.widget.state = values
        form.widget.on_changed.connect(lambda d: self.check_differences(
            d,
            self._values_dict[schema["title"].lower()],
        ))

        return form

    def _values_changed(self, page, new_dict, old_dict):
        """Loops through each setting in a page to determine if it changed.

        Parameters
        ----------
        new_dict : dict
            Dict that has the most recent changes by user. Each key is a setting value
            and each item is the value.
        old_dict : dict
            Dict wtih values set at the begining of preferences dialog session.

        """
        for setting_name, value in new_dict.items():
            if value != old_dict[setting_name]:
                self._setting_changed_dict[page][setting_name] = value
            elif (value == old_dict[setting_name]
                  and setting_name in self._setting_changed_dict[page]):
                self._setting_changed_dict[page].pop(setting_name)

    def check_differences(self, new_dict, old_dict):
        """Changes settings in settings manager with changes from dialog.

        Parameters
        ----------
        new_dict : dict
            Dict that has the most recent changes by user. Each key is a setting parameter
            and each item is the value.
        old_dict : dict
            Dict wtih values set at the beginning of the preferences dialog session.
        """
        page = self._list.currentItem().text().split(" ")[0].lower()
        self._values_changed(page, new_dict, old_dict)
        different_values = self._setting_changed_dict[page]

        if len(different_values) > 0:
            # change the values in SETTINGS
            for setting_name, value in different_values.items():
                try:
                    setattr(SETTINGS._settings[page], setting_name, value)
                    self._values_dict[page] = new_dict
                except:  # noqa: E722
                    continue
示例#11
0
class SegmentationInfoDialog(QWidget):
    def __init__(self,
                 settings: StackSettings,
                 set_parameters: Callable[[str, dict], None],
                 additional_text=None):
        """

        :param settings:
        :param set_parameters: Function which set parameters of chosen in dialog.
        :param additional_text: Additional text on top of Window.
        """
        super().__init__()
        self.settings = settings
        self.parameters_dict = None
        self.set_parameters = set_parameters
        self.components = QListWidget()
        self.components.currentItemChanged.connect(self.change_component_info)
        self.description = QPlainTextEdit()
        self.description.setReadOnly(True)
        self.close_btn = QPushButton("Close")
        self.close_btn.clicked.connect(self.close)
        self.set_parameters_btn = QPushButton("Reuse parameters")
        self.set_parameters_btn.clicked.connect(self.set_parameter_action)
        self.additional_text_label = QLabel(additional_text)
        layout = QGridLayout()
        layout.addWidget(self.additional_text_label, 0, 0, 1, 2)
        if not additional_text:
            self.additional_text_label.setVisible(False)

        layout.addWidget(QLabel("Components:"), 1, 0)
        layout.addWidget(QLabel("segmentation parameters:"), 1, 1)
        layout.addWidget(self.components, 2, 0)
        layout.addWidget(self.description, 2, 1)
        layout.addWidget(self.close_btn, 3, 0)
        layout.addWidget(self.set_parameters_btn, 3, 1)
        self.setLayout(layout)
        self.setWindowTitle("Parameters preview")

    def set_parameters_dict(self, val: Optional[Dict[int,
                                                     SegmentationProfile]]):
        self.parameters_dict = val

    def set_additional_text(self, text):
        self.additional_text_label.setText(text)
        self.additional_text_label.setVisible(bool(text))

    @property
    def get_parameters(self):
        if self.parameters_dict:
            return self.parameters_dict
        return self.settings.components_parameters_dict

    def change_component_info(self):
        if self.components.currentItem() is None:
            return
        text = self.components.currentItem().text()
        parameters = self.get_parameters[int(text)]
        if parameters is None:
            self.description.setPlainText("None")
        else:
            self.description.setPlainText(
                f"Component {text}\n" +
                parameters.pretty_print(mask_algorithm_dict))

    def set_parameter_action(self):
        if self.components.currentItem() is None:
            return
        text = self.components.currentItem().text()
        parameters = self.get_parameters[int(text)]
        self.set_parameters(parameters.algorithm, parameters.values)

    def event(self, event: QEvent):
        if event.type() == QEvent.WindowActivate:
            index = self.components.currentRow()
            self.components.clear()
            self.components.addItems(list(map(str,
                                              self.get_parameters.keys())))
            self.components.setCurrentRow(index)
        return super().event(event)
示例#12
0
class ExportDialog(QDialog):
    def __init__(self, export_dict, viewer):
        super(ExportDialog, self).__init__()
        self.setWindowTitle("Export")
        self.export_dict = export_dict
        self.viewer = viewer()
        self.list_view = QListWidget()
        self.check_state = np.zeros(len(export_dict), dtype=np.bool)
        self.check_state[...] = True
        for el in sorted(export_dict.keys()):
            item = QListWidgetItem(el)
            # noinspection PyTypeChecker
            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Checked)
            self.list_view.addItem(item)

        self.checked_num = len(export_dict)

        self.export_btn = QPushButton("Export")
        self.cancel_btn = QPushButton("Cancel")
        self.check_btn = QPushButton("Check all")
        self.uncheck_btn = QPushButton("Uncheck all")

        self.cancel_btn.clicked.connect(self.close)
        self.export_btn.clicked.connect(self.accept)
        self.check_btn.clicked.connect(self.check_all)
        self.uncheck_btn.clicked.connect(self.uncheck_all)

        self.list_view.itemSelectionChanged.connect(self.preview)
        self.list_view.itemChanged.connect(self.checked_change)

        layout = QVBoxLayout()
        info_layout = QHBoxLayout()
        info_layout.addWidget(self.list_view)
        info_layout.addWidget(self.viewer)
        layout.addLayout(info_layout)
        btn_layout = QHBoxLayout()
        btn_layout.addWidget(self.check_btn)
        btn_layout.addWidget(self.uncheck_btn)
        btn_layout.addStretch()
        btn_layout.addWidget(self.export_btn)
        btn_layout.addWidget(self.cancel_btn)
        layout.addLayout(btn_layout)
        self.setLayout(layout)

    def checked_change(self, item):
        if item.checkState() == Qt.Unchecked:
            self.checked_num -= 1
        else:
            self.checked_num += 1
        if self.checked_num == 0:
            self.export_btn.setDisabled(True)
        else:
            self.export_btn.setEnabled(True)

    def get_checked(self):
        res = []
        for i in range(self.list_view.count()):
            it = self.list_view.item(i)
            if it.checkState() == Qt.Checked:
                res.append(str(it.text()))
        return res

    def preview(self):
        name = str(self.list_view.currentItem().text())
        self.viewer.preview_object(self.export_dict[name])

    def check_change(self):
        item = self.list_view.currentItem()  # type: QListWidgetItem
        index = self.list_view.currentRow()
        checked = item.checkState() == Qt.Checked
        self.check_state[index] = checked
        self.export_btn.setEnabled(np.any(self.check_state))

    def uncheck_all(self):
        for index in range(self.list_view.count()):
            item = self.list_view.item(index)
            item.setCheckState(Qt.Unchecked)
        self.check_state[...] = False
        self.export_btn.setDisabled(True)
        self.checked_num = 0

    def check_all(self):
        for index in range(self.list_view.count()):
            item = self.list_view.item(index)
            item.setCheckState(Qt.Checked)
        self.checked_num = len(self.export_dict)
        self.check_state[...] = True
        self.export_btn.setDisabled(False)

    def get_export_list(self):
        res = []
        for num in range(self.list_view.count()):
            item = self.list_view.item(num)
            if item.checkState() == Qt.Checked:
                res.append(str(item.text()))
        return res
class CreatePlan(QWidget):

    plan_created = Signal()
    plan_node_changed = Signal()

    def __init__(self, settings: PartSettings):
        super().__init__()
        self.settings = settings
        self.save_translate_dict: typing.Dict[str, SaveBase] = {
            x.get_short_name(): x
            for x in save_dict.values()
        }
        self.plan = PlanPreview(self)
        self.save_plan_btn = QPushButton("Save")
        self.clean_plan_btn = QPushButton("Remove all")
        self.remove_btn = QPushButton("Remove")
        self.update_element_chk = QCheckBox("Update element")
        self.change_root = EnumComboBox(RootType)
        self.save_choose = QComboBox()
        self.save_choose.addItem("<none>")
        self.save_choose.addItems(list(self.save_translate_dict.keys()))
        self.save_btn = QPushButton("Save")
        self.segment_profile = QListWidget()
        self.pipeline_profile = QListWidget()
        self.segment_stack = QTabWidget()
        self.segment_stack.addTab(self.segment_profile, "Profile")
        self.segment_stack.addTab(self.pipeline_profile, "Pipeline")
        self.generate_mask_btn = QPushButton("Add mask")
        self.generate_mask_btn.setToolTip("Mask need to have unique name")
        self.mask_name = QLineEdit()
        self.mask_operation = EnumComboBox(MaskOperation)

        self.chanel_num = QSpinBox()
        self.choose_channel_for_measurements = QComboBox()
        self.choose_channel_for_measurements.addItems(
            ["Same as segmentation"] +
            [str(x + 1) for x in range(MAX_CHANNEL_NUM)])
        self.units_choose = EnumComboBox(Units)
        self.units_choose.set_value(self.settings.get("units_value", Units.nm))
        self.chanel_num.setRange(0, 10)
        self.expected_node_type = None
        self.save_constructor = None

        self.chose_profile_btn = QPushButton("Add Profile")
        self.get_big_btn = QPushButton("Leave the biggest")
        self.get_big_btn.hide()
        self.add_new_segmentation_btn = QPushButton("Add new profile")
        self.get_big_btn.setDisabled(True)
        self.add_new_segmentation_btn.setDisabled(True)
        self.measurements_list = QListWidget(self)
        self.measurement_name_prefix = QLineEdit(self)
        self.add_calculation_btn = QPushButton("Add measurement calculation")
        self.information = QTextEdit()
        self.information.setReadOnly(True)

        self.protect = False
        self.mask_set = set()
        self.calculation_plan = CalculationPlan()
        self.plan.set_plan(self.calculation_plan)
        self.segmentation_mask = MaskWidget(settings)
        self.file_mask = FileMask()

        self.change_root.currentIndexChanged.connect(self.change_root_type)
        self.save_choose.currentTextChanged.connect(self.save_changed)
        self.measurements_list.currentTextChanged.connect(
            self.show_measurement)
        self.segment_profile.currentTextChanged.connect(self.show_segment)
        self.measurements_list.currentTextChanged.connect(
            self.show_measurement_info)
        self.segment_profile.currentTextChanged.connect(self.show_segment_info)
        self.pipeline_profile.currentTextChanged.connect(
            self.show_segment_info)
        self.pipeline_profile.currentTextChanged.connect(self.show_segment)
        self.mask_name.textChanged.connect(self.mask_name_changed)
        self.generate_mask_btn.clicked.connect(self.create_mask)
        self.clean_plan_btn.clicked.connect(self.clean_plan)
        self.remove_btn.clicked.connect(self.remove_element)
        self.mask_name.textChanged.connect(self.mask_text_changed)
        self.chose_profile_btn.clicked.connect(self.add_segmentation)
        self.get_big_btn.clicked.connect(self.add_leave_biggest)
        self.add_calculation_btn.clicked.connect(self.add_measurement)
        self.save_plan_btn.clicked.connect(self.add_calculation_plan)
        # self.forgot_mask_btn.clicked.connect(self.forgot_mask)
        # self.cmap_save_btn.clicked.connect(self.save_to_cmap)
        self.save_btn.clicked.connect(self.add_save_to_project)
        self.update_element_chk.stateChanged.connect(self.mask_text_changed)
        self.update_element_chk.stateChanged.connect(self.show_measurement)
        self.update_element_chk.stateChanged.connect(self.show_segment)
        self.update_element_chk.stateChanged.connect(self.update_names)
        self.segment_stack.currentChanged.connect(
            self.change_segmentation_table)

        plan_box = QGroupBox("Prepare workflow:")
        lay = QVBoxLayout()
        lay.addWidget(self.plan)
        bt_lay = QGridLayout()
        bt_lay.setSpacing(0)
        bt_lay.addWidget(self.save_plan_btn, 0, 0)
        bt_lay.addWidget(self.clean_plan_btn, 0, 1)
        bt_lay.addWidget(self.remove_btn, 1, 0)
        bt_lay.addWidget(self.update_element_chk, 1, 1)
        lay.addLayout(bt_lay)
        plan_box.setLayout(lay)
        plan_box.setStyleSheet(group_sheet)

        other_box = QGroupBox("Other operations:")
        other_box.setContentsMargins(0, 0, 0, 0)
        bt_lay = QVBoxLayout()
        bt_lay.setSpacing(0)
        bt_lay.addWidget(QLabel("Root type:"))
        bt_lay.addWidget(self.change_root)
        bt_lay.addStretch(1)
        bt_lay.addWidget(QLabel("Saving:"))
        bt_lay.addWidget(self.save_choose)
        bt_lay.addWidget(self.save_btn)
        other_box.setLayout(bt_lay)
        other_box.setStyleSheet(group_sheet)

        mask_box = QGroupBox("Use mask from:")
        mask_box.setStyleSheet(group_sheet)
        self.mask_stack = QTabWidget()

        self.mask_stack.addTab(stretch_widget(self.file_mask), "File")
        self.mask_stack.addTab(stretch_widget(self.segmentation_mask),
                               "Current segmentation")
        self.mask_stack.addTab(stretch_widget(self.mask_operation),
                               "Operations on masks")
        self.mask_stack.setTabToolTip(
            2,
            "Allows to create mask which is based on masks previously added to plan."
        )

        lay = QGridLayout()
        lay.setSpacing(0)
        lay.addWidget(self.mask_stack, 0, 0, 1, 2)
        label = QLabel("Mask name:")
        label.setToolTip(
            "Needed if you would like to reuse this mask in tab 'Operations on masks'"
        )
        self.mask_name.setToolTip(
            "Needed if you would like to reuse this mask in tab 'Operations on masks'"
        )
        lay.addWidget(label, 1, 0)
        lay.addWidget(self.mask_name, 1, 1)
        lay.addWidget(self.generate_mask_btn, 2, 0, 1, 2)
        mask_box.setLayout(lay)

        segment_box = QGroupBox("Segmentation:")
        segment_box.setStyleSheet(group_sheet)
        lay = QVBoxLayout()
        lay.setSpacing(0)
        lay.addWidget(self.segment_stack)
        lay.addWidget(self.chose_profile_btn)
        lay.addWidget(self.get_big_btn)
        lay.addWidget(self.add_new_segmentation_btn)
        segment_box.setLayout(lay)

        measurement_box = QGroupBox("Set of measurements:")
        measurement_box.setStyleSheet(group_sheet)
        lay = QGridLayout()
        lay.setSpacing(0)
        lay.addWidget(self.measurements_list, 0, 0, 1, 2)
        lab = QLabel("Name prefix:")
        lab.setToolTip("Prefix added before each column name")
        lay.addWidget(lab, 1, 0)
        lay.addWidget(self.measurement_name_prefix, 1, 1)
        lay.addWidget(QLabel("Channel:"), 2, 0)
        lay.addWidget(self.choose_channel_for_measurements, 2, 1)
        lay.addWidget(QLabel("Units:"))
        lay.addWidget(self.units_choose, 3, 1)
        lay.addWidget(self.add_calculation_btn, 4, 0, 1, 2)
        measurement_box.setLayout(lay)

        info_box = QGroupBox("Information")
        info_box.setStyleSheet(group_sheet)
        lay = QVBoxLayout()
        lay.addWidget(self.information)
        info_box.setLayout(lay)

        layout = QGridLayout()
        fst_col = QVBoxLayout()
        fst_col.addWidget(plan_box, 1)
        fst_col.addWidget(mask_box)
        layout.addWidget(plan_box, 0, 0, 5, 1)
        # layout.addWidget(plan_box, 0, 0, 3, 1)
        # layout.addWidget(mask_box, 3, 0, 2, 1)
        # layout.addWidget(segmentation_mask_box, 1, 1)
        layout.addWidget(mask_box, 0, 2, 1, 2)
        layout.addWidget(other_box, 0, 1)
        layout.addWidget(segment_box, 1, 1, 1, 2)
        layout.addWidget(measurement_box, 1, 3)
        layout.addWidget(info_box, 3, 1, 1, 3)
        self.setLayout(layout)

        self.generate_mask_btn.setDisabled(True)
        self.chose_profile_btn.setDisabled(True)
        self.add_calculation_btn.setDisabled(True)

        self.mask_allow = False
        self.segment_allow = False
        self.file_mask_allow = False
        self.node_type = NodeType.root
        self.node_name = ""
        self.plan_node_changed.connect(self.mask_text_changed)
        self.plan.changed_node.connect(self.node_type_changed)
        self.plan_node_changed.connect(self.show_segment)
        self.plan_node_changed.connect(self.show_measurement)
        self.plan_node_changed.connect(self.mask_stack_change)
        self.mask_stack.currentChanged.connect(self.mask_stack_change)
        self.file_mask.value_changed.connect(self.mask_stack_change)
        self.mask_name.textChanged.connect(self.mask_stack_change)
        self.node_type_changed()

    def change_root_type(self):
        value: RootType = self.change_root.get_value()
        self.calculation_plan.set_root_type(value)
        self.plan.update_view()

    def change_segmentation_table(self):
        index = self.segment_stack.currentIndex()
        text = self.segment_stack.tabText(index)
        if self.update_element_chk.isChecked():
            self.chose_profile_btn.setText("Replace " + text)
        else:
            self.chose_profile_btn.setText("Add " + text)
        self.segment_profile.setCurrentItem(None)
        self.pipeline_profile.setCurrentItem(None)

    def save_changed(self, text):
        text = str(text)
        if text == "<none>":
            self.save_btn.setText("Save")
            self.save_btn.setToolTip("Choose file type")
            self.expected_node_type = None
            self.save_constructor = None
        else:
            save_class = self.save_translate_dict.get(text, None)
            if save_class is None:
                self.save_choose.setCurrentText("<none>")
                return
            self.save_btn.setText(f"Save to {save_class.get_short_name()}")
            self.save_btn.setToolTip("Choose mask create in plan view")
            if save_class.need_mask():
                self.expected_node_type = NodeType.mask
            elif save_class.need_segmentation():
                self.expected_node_type = NodeType.segment
            else:
                self.expected_node_type = NodeType.root
            self.save_constructor = Save
        self.save_activate()

    def save_activate(self):
        self.save_btn.setDisabled(True)
        if self.node_type == self.expected_node_type:
            self.save_btn.setEnabled(True)
            return

    def segmentation_from_project(self):
        self.calculation_plan.add_step(Operations.reset_to_base)
        self.plan.update_view()

    def update_names(self):
        if self.update_element_chk.isChecked():
            self.chose_profile_btn.setText("Replace Profile")
            self.add_calculation_btn.setText("Replace set of measurements")
            self.generate_mask_btn.setText("Replace mask")
        else:
            self.chose_profile_btn.setText("Add Profile")
            self.add_calculation_btn.setText("Add set of measurements")
            self.generate_mask_btn.setText("Generate mask")

    def node_type_changed(self):
        # self.cmap_save_btn.setDisabled(True)
        self.save_btn.setDisabled(True)
        self.node_name = ""
        if self.plan.currentItem() is None:
            self.mask_allow = False
            self.file_mask_allow = False
            self.segment_allow = False
            self.remove_btn.setDisabled(True)
            self.plan_node_changed.emit()
            logging.debug("[node_type_changed] return")
            return
        node_type = self.calculation_plan.get_node_type()
        self.node_type = node_type
        if node_type in [
                NodeType.file_mask, NodeType.mask, NodeType.segment,
                NodeType.measurement, NodeType.save
        ]:
            self.remove_btn.setEnabled(True)
        else:
            self.remove_btn.setEnabled(False)
        if node_type == NodeType.mask or node_type == NodeType.file_mask:
            self.mask_allow = False
            self.segment_allow = True
            self.file_mask_allow = False
            self.node_name = self.calculation_plan.get_node().operation.name
        elif node_type == NodeType.segment:
            self.mask_allow = True
            self.segment_allow = False
            self.file_mask_allow = False
            self.save_btn.setEnabled(True)
            # self.cmap_save_btn.setEnabled(True)
        elif node_type == NodeType.root:
            self.mask_allow = False
            self.segment_allow = True
            self.file_mask_allow = True
        elif node_type == NodeType.none or node_type == NodeType.measurement or node_type == NodeType.save:
            self.mask_allow = False
            self.segment_allow = False
            self.file_mask_allow = False
        self.save_activate()
        self.plan_node_changed.emit()

    def add_save_to_project(self):
        save_class = self.save_translate_dict.get(
            self.save_choose.currentText(), None)
        if save_class is None:
            QMessageBox.warning(self, "Save problem", "Not found save class")
        dial = FormDialog([
            AlgorithmProperty("suffix", "File suffix", ""),
            AlgorithmProperty("directory", "Sub directory", "")
        ] + save_class.get_fields())
        if dial.exec():
            values = dial.get_values()
            suffix = values["suffix"]
            directory = values["directory"]
            del values["suffix"]
            del values["directory"]
            save_elem = Save(suffix, directory, save_class.get_name(),
                             save_class.get_short_name(), values)
            if self.update_element_chk.isChecked():
                self.calculation_plan.replace_step(save_elem)
            else:
                self.calculation_plan.add_step(save_elem)
            self.plan.update_view()

    def create_mask(self):
        text = str(self.mask_name.text()).strip()

        if text != "" and text in self.mask_set:
            QMessageBox.warning(self, "Already exists",
                                "Mask with this name already exists",
                                QMessageBox.Ok)
            return
        if _check_widget(self.mask_stack, EnumComboBox):  # existing mask
            mask_dialog = TwoMaskDialog
            if self.mask_operation.get_value(
            ) == MaskOperation.mask_intersection:  # Mask intersection
                MaskConstruct = MaskIntersection
            else:
                MaskConstruct = MaskSum
            dial = mask_dialog(self.mask_set)
            if not dial.exec():
                return
            names = dial.get_result()

            mask_ob = MaskConstruct(text, *names)
        elif _check_widget(self.mask_stack, MaskWidget):
            mask_ob = MaskCreate(text,
                                 self.segmentation_mask.get_mask_property())
        elif _check_widget(self.mask_stack, FileMask):
            mask_ob = self.file_mask.get_value(text)
        else:
            raise ValueError("Unknowsn widget")

        if self.update_element_chk.isChecked():
            node = self.calculation_plan.get_node()
            name = node.operation.name
            if name in self.calculation_plan.get_reused_mask(
            ) and name != text:
                QMessageBox.warning(
                    self, "Cannot remove",
                    f"Cannot remove mask '{name}' from plan because it is used in other elements"
                )
                return

            self.mask_set.remove(name)
            self.mask_set.add(mask_ob.name)
            self.calculation_plan.replace_step(mask_ob)
        else:
            self.mask_set.add(mask_ob.name)
            self.calculation_plan.add_step(mask_ob)
        self.plan.update_view()
        self.mask_text_changed()

    def mask_stack_change(self):
        node_type = self.calculation_plan.get_node_type()
        if self.update_element_chk.isChecked() and node_type not in [
                NodeType.mask, NodeType.file_mask
        ]:
            self.generate_mask_btn.setDisabled(True)
        text = self.mask_name.text()
        update = self.update_element_chk.isChecked()
        if self.node_type == NodeType.none:
            self.generate_mask_btn.setDisabled(True)
            return
        operation = self.calculation_plan.get_node().operation
        if (not update and isinstance(operation, (MaskMapper, MaskBase))
                and self.calculation_plan.get_node().operation.name == text):
            self.generate_mask_btn.setDisabled(True)
            return
        if _check_widget(self.mask_stack, EnumComboBox):  # reuse mask
            if len(self.mask_set) > 1 and (
                (not update and node_type == NodeType.root) or
                (update and node_type == NodeType.file_mask)):
                self.generate_mask_btn.setEnabled(True)
            else:
                self.generate_mask_btn.setEnabled(False)
            self.generate_mask_btn.setToolTip(
                "Need at least two named mask and root selected")
        elif _check_widget(self.mask_stack,
                           MaskWidget):  # mask from segmentation
            if (not update and node_type == NodeType.segment) or (
                    update and node_type == NodeType.mask):
                self.generate_mask_btn.setEnabled(True)
            else:
                self.generate_mask_btn.setEnabled(False)
            self.generate_mask_btn.setToolTip("Select segmentation")
        else:
            if (not update and node_type == NodeType.root) or (
                    update and node_type == NodeType.file_mask):
                self.generate_mask_btn.setEnabled(self.file_mask.is_valid())
            else:
                self.generate_mask_btn.setEnabled(False)
            self.generate_mask_btn.setToolTip("Need root selected")

    def mask_name_changed(self, text):
        if str(text) in self.mask_set:
            self.generate_mask_btn.setDisabled(True)
        else:
            self.generate_mask_btn.setDisabled(False)

    def add_leave_biggest(self):
        profile = self.calculation_plan.get_node().operation
        profile.leave_biggest_swap()
        self.calculation_plan.replace_step(profile)
        self.plan.update_view()

    def add_segmentation(self):
        if self.segment_stack.currentIndex() == 0:
            text = str(self.segment_profile.currentItem().text())
            if text not in self.settings.segmentation_profiles:
                self.refresh_all_profiles()
                return
            profile = self.settings.segmentation_profiles[text]
            if self.update_element_chk.isChecked():
                self.calculation_plan.replace_step(profile)
            else:
                self.calculation_plan.add_step(profile)
            self.plan.update_view()

        else:  # self.segment_stack.currentIndex() == 1
            text = self.pipeline_profile.currentItem().text()
            segmentation_pipeline = self.settings.segmentation_pipelines[text]
            pos = self.calculation_plan.current_pos[:]
            old_pos = self.calculation_plan.current_pos[:]
            for el in segmentation_pipeline.mask_history:
                self.calculation_plan.add_step(el.segmentation)
                self.plan.update_view()
                node = self.calculation_plan.get_node(pos)
                pos.append(len(node.children) - 1)
                self.calculation_plan.set_position(pos)
                self.calculation_plan.add_step(MaskCreate(
                    "", el.mask_property))
                self.plan.update_view()
                pos.append(0)
                self.calculation_plan.set_position(pos)
            self.calculation_plan.add_step(segmentation_pipeline.segmentation)
            self.calculation_plan.set_position(old_pos)
            self.plan.update_view()

    def add_measurement(self):
        text = str(self.measurements_list.currentItem().text())
        measurement_copy = deepcopy(self.settings.measurement_profiles[text])
        prefix = str(self.measurement_name_prefix.text()).strip()
        channel = self.choose_channel_for_measurements.currentIndex() - 1
        measurement_copy.name_prefix = prefix
        # noinspection PyTypeChecker
        measurement_calculate = MeasurementCalculate(
            channel=channel,
            statistic_profile=measurement_copy,
            name_prefix=prefix,
            units=self.units_choose.get_value())
        if self.update_element_chk.isChecked():
            self.calculation_plan.replace_step(measurement_calculate)
        else:
            self.calculation_plan.add_step(measurement_calculate)
        self.plan.update_view()

    def remove_element(self):
        conflict_mask, used_mask = self.calculation_plan.get_file_mask_names()
        if len(conflict_mask) > 0:
            logging.info("Mask in use")
            QMessageBox.warning(
                self, "In use", "Masks {} are used in other places".format(
                    ", ".join(conflict_mask)))
            return
        self.mask_set -= used_mask
        self.calculation_plan.remove_step()
        self.plan.update_view()

    def clean_plan(self):
        self.calculation_plan = CalculationPlan()
        self.plan.set_plan(self.calculation_plan)
        self.node_type_changed()
        self.mask_set = set()

    def mask_text_changed(self):
        name = str(self.mask_name.text()).strip()
        self.generate_mask_btn.setDisabled(True)
        # load mask from file
        if not self.update_element_chk.isChecked():
            # generate mask from segmentation
            if self.mask_allow and (name == "" or name not in self.mask_set):
                self.generate_mask_btn.setEnabled(True)
        else:
            if self.node_type != NodeType.file_mask and self.node_type != NodeType.mask:
                return
            # generate mask from segmentation
            if self.node_type == NodeType.mask and (
                    name == "" or name == self.node_name
                    or name not in self.mask_set):
                self.generate_mask_btn.setEnabled(True)

    def add_calculation_plan(self, text=None):
        if text is None or isinstance(text, bool):
            text, ok = QInputDialog.getText(self, "Plan title",
                                            "Set plan title")
        else:
            text, ok = QInputDialog.getText(
                self,
                "Plan title",
                "Set plan title. Previous ({}) is already in use".format(text),
                text=text)
        text = text.strip()
        if ok:
            if text == "":
                QMessageBox.information(
                    self, "Name cannot be empty",
                    "Name cannot be empty, Please set correct name",
                    QMessageBox.Ok)
                self.add_calculation_plan()
                return
            if text in self.settings.batch_plans:
                res = QMessageBox.information(
                    self,
                    "Name already in use",
                    "Name already in use. Would like to overwrite?",
                    QMessageBox.Yes | QMessageBox.No,
                )
                if res == QMessageBox.No:
                    self.add_calculation_plan(text)
                    return
            plan = copy(self.calculation_plan)
            plan.set_name(text)
            self.settings.batch_plans[text] = plan
            self.settings.dump()
            self.plan_created.emit()

    @staticmethod
    def get_index(item: QListWidgetItem, new_values: typing.List[str]) -> int:
        if item is None:
            return -1
        text = item.text()
        try:
            return new_values.index(text)
        except ValueError:
            return -1

    @staticmethod
    def refresh_profiles(list_widget: QListWidget,
                         new_values: typing.List[str], index: int):
        list_widget.clear()
        list_widget.addItems(new_values)
        if index != -1:
            list_widget.setCurrentRow(index)

    def showEvent(self, event):
        self.refresh_all_profiles()

    def refresh_all_profiles(self):
        new_measurements = list(
            sorted(self.settings.measurement_profiles.keys()))
        new_segment = list(sorted(self.settings.segmentation_profiles.keys()))
        new_pipelines = list(
            sorted(self.settings.segmentation_pipelines.keys()))
        measurement_index = self.get_index(
            self.measurements_list.currentItem(), new_measurements)
        segment_index = self.get_index(self.segment_profile.currentItem(),
                                       new_segment)
        pipeline_index = self.get_index(self.pipeline_profile.currentItem(),
                                        new_pipelines)
        self.protect = True
        self.refresh_profiles(self.measurements_list, new_measurements,
                              measurement_index)
        self.refresh_profiles(self.segment_profile, new_segment, segment_index)
        self.refresh_profiles(self.pipeline_profile, new_pipelines,
                              pipeline_index)
        self.protect = False

    def show_measurement_info(self, text=None):
        if self.protect:
            return
        if text is None:
            if self.measurements_list.currentItem() is not None:
                text = str(self.measurements_list.currentItem().text())
            else:
                return
        profile = self.settings.measurement_profiles[text]
        self.information.setText(str(profile))

    def show_measurement(self):
        if self.update_element_chk.isChecked():
            if self.node_type == NodeType.measurement:
                self.add_calculation_btn.setEnabled(True)
            else:
                self.add_calculation_btn.setDisabled(True)
        else:
            if self.measurements_list.currentItem() is not None:
                self.add_calculation_btn.setEnabled(self.mask_allow)
            else:
                self.add_calculation_btn.setDisabled(True)

    def show_segment_info(self, text=None):
        if self.protect:
            return
        if text == "":
            return
        if self.segment_stack.currentIndex() == 0:
            if text is None:
                if self.segment_profile.currentItem() is not None:
                    text = str(self.segment_profile.currentItem().text())
                else:
                    return
            profile = self.settings.segmentation_profiles[text]
        else:
            if text is None:
                if self.pipeline_profile.currentItem() is not None:
                    text = str(self.pipeline_profile.currentItem().text())
                else:
                    return
            profile = self.settings.segmentation_pipelines[text]
        self.information.setText(profile.pretty_print(analysis_algorithm_dict))

    def show_segment(self):
        if self.update_element_chk.isChecked(
        ) and self.segment_stack.currentIndex() == 0:
            self.get_big_btn.setDisabled(True)
            if self.node_type == NodeType.segment:
                self.chose_profile_btn.setEnabled(True)
            else:
                self.chose_profile_btn.setDisabled(True)
        else:
            if self.node_type == NodeType.segment:
                self.get_big_btn.setEnabled(True)
            else:
                self.get_big_btn.setDisabled(True)
            if self.segment_stack.currentIndex() == 0:
                if self.segment_profile.currentItem() is not None:
                    self.chose_profile_btn.setEnabled(self.segment_allow)
                else:
                    self.chose_profile_btn.setDisabled(True)
            else:
                if self.pipeline_profile.currentItem() is not None:
                    self.chose_profile_btn.setEnabled(self.segment_allow)
                else:
                    self.chose_profile_btn.setDisabled(True)

    def edit_plan(self):
        plan = self.sender().plan_to_edit  # type: CalculationPlan
        self.calculation_plan = copy(plan)
        self.plan.set_plan(self.calculation_plan)
        self.mask_set.clear()
        self.calculation_plan.set_position([])
        self.mask_set.update(self.calculation_plan.get_mask_names())
class CalculateInfo(QWidget):
    """
    "widget to show information about plans and allow to se plan details
    :type settings: Settings
    """

    plan_to_edit_signal = Signal()

    def __init__(self, settings: PartSettings):
        super(CalculateInfo, self).__init__()
        self.settings = settings
        self.calculate_plans = QListWidget(self)
        self.plan_view = PlanPreview(self)
        self.delete_plan_btn = QPushButton("Delete")
        self.edit_plan_btn = QPushButton("Edit")
        self.export_plans_btn = QPushButton("Export")
        self.import_plans_btn = QPushButton("Import")
        info_layout = QVBoxLayout()
        info_butt_layout = QGridLayout()
        info_butt_layout.setSpacing(0)
        info_butt_layout.addWidget(self.delete_plan_btn, 1, 1)
        info_butt_layout.addWidget(self.edit_plan_btn, 0, 1)
        info_butt_layout.addWidget(self.export_plans_btn, 1, 0)
        info_butt_layout.addWidget(self.import_plans_btn, 0, 0)
        info_layout.addLayout(info_butt_layout)
        info_chose_layout = QVBoxLayout()
        info_chose_layout.setSpacing(2)
        info_chose_layout.addWidget(QLabel("List of workflows:"))
        info_chose_layout.addWidget(self.calculate_plans)
        info_chose_layout.addWidget(QLabel("Preview:"))
        info_chose_layout.addWidget(self.plan_view)
        info_layout.addLayout(info_chose_layout)
        self.setLayout(info_layout)
        self.calculate_plans.addItems(
            list(sorted(self.settings.batch_plans.keys())))
        self.protect = False
        self.plan_to_edit = None

        self.plan_view.header().close()
        self.calculate_plans.currentTextChanged.connect(self.plan_preview)
        self.delete_plan_btn.clicked.connect(self.delete_plan)
        self.edit_plan_btn.clicked.connect(self.edit_plan)
        self.export_plans_btn.clicked.connect(self.export_plans)
        self.import_plans_btn.clicked.connect(self.import_plans)

    def update_plan_list(self):
        new_plan_list = list(sorted(self.settings.batch_plans.keys()))
        if self.calculate_plans.currentItem() is not None:
            text = str(self.calculate_plans.currentItem().text())
            try:
                index = new_plan_list.index(text)
            except ValueError:
                index = -1
        else:
            index = -1
        self.protect = True
        self.calculate_plans.clear()
        self.calculate_plans.addItems(new_plan_list)
        if index != -1:
            self.calculate_plans.setCurrentRow(index)
        else:
            pass
            # self.plan_view.setText("")
        self.protect = False

    def export_plans(self):
        choose = ExportDialog(self.settings.batch_plans, PlanPreview)
        if not choose.exec_():
            return
        dial = QFileDialog(self, "Export calculation plans")
        dial.setFileMode(QFileDialog.AnyFile)
        dial.setAcceptMode(QFileDialog.AcceptSave)
        dial.setDirectory(
            dial.setDirectory(
                self.settings.get("io.batch_plan_directory",
                                  str(Path.home()))))
        dial.setNameFilter("Calculation plans (*.json)")
        dial.setDefaultSuffix("json")
        dial.selectFile("calculation_plans.json")
        dial.setHistory(dial.history() + self.settings.get_path_history())
        if dial.exec_():
            file_path = str(dial.selectedFiles()[0])
            self.settings.set("io.batch_plan_directory",
                              os.path.dirname(file_path))
            self.settings.add_path_history(os.path.dirname(file_path))
            data = {
                x: self.settings.batch_plans[x]
                for x in choose.get_export_list()
            }
            with open(file_path, "w") as ff:
                json.dump(data,
                          ff,
                          cls=self.settings.json_encoder_class,
                          indent=2)

    def import_plans(self):
        dial = QFileDialog(self, "Import calculation plans")
        dial.setFileMode(QFileDialog.ExistingFile)
        dial.setAcceptMode(QFileDialog.AcceptOpen)
        dial.setDirectory(
            self.settings.get("io.open_directory", str(Path.home())))
        dial.setNameFilter("Calculation plans (*.json)")
        dial.setDefaultSuffix("json")
        dial.setHistory(dial.history() + self.settings.get_path_history())
        if dial.exec_():
            file_path = dial.selectedFiles()[0]
            plans, err = self.settings.load_part(file_path)
            self.settings.set("io.batch_plan_directory",
                              os.path.dirname(file_path))
            self.settings.add_path_history(os.path.dirname(file_path))
            if err:
                QMessageBox.warning(
                    self, "Import error",
                    "error during importing, part of data were filtered.")
            choose = ImportDialog(plans, self.settings.batch_plans,
                                  PlanPreview)
            if choose.exec_():
                for original_name, final_name in choose.get_import_list():
                    self.settings.batch_plans[final_name] = plans[
                        original_name]
                self.update_plan_list()

    def delete_plan(self):
        if self.calculate_plans.currentItem() is None:
            return
        text = str(self.calculate_plans.currentItem().text())
        if text == "":
            return
        if text in self.settings.batch_plans:
            del self.settings.batch_plans[text]
        self.update_plan_list()
        self.plan_view.clear()

    def edit_plan(self):
        if self.calculate_plans.currentItem() is None:
            return
        text = str(self.calculate_plans.currentItem().text())
        if text == "":
            return
        if text in self.settings.batch_plans:
            self.plan_to_edit = self.settings.batch_plans[text]
            self.plan_to_edit_signal.emit()

    def plan_preview(self, text):
        if self.protect:
            return
        text = str(text)
        if text.strip() == "":
            return
        plan = self.settings.batch_plans[str(text)]  # type: CalculationPlan
        self.plan_view.set_plan(plan)
示例#15
0
class ApplicationsDialog(QDialog):
    """Dialog for selection of installed system/user applications."""
    def __init__(self, parent=None):
        """Dialog for selection of installed system/user applications."""
        super(ApplicationsDialog, self).__init__(parent=parent)

        # Widgets
        self.label = QLabel()
        self.label_browse = QLabel()
        self.edit_filter = QLineEdit()
        self.list = QListWidget()
        self.button_browse = QPushButton(_('Browse...'))
        self.button_box = QDialogButtonBox(QDialogButtonBox.Ok
                                           | QDialogButtonBox.Cancel)
        self.button_ok = self.button_box.button(QDialogButtonBox.Ok)
        self.button_cancel = self.button_box.button(QDialogButtonBox.Cancel)

        # Widget setup
        self.setWindowTitle(_('Applications'))
        self.edit_filter.setPlaceholderText(_('Type to filter by name'))
        self.list.setIconSize(QSize(16, 16))  # FIXME: Use metrics

        # Layout
        layout = QVBoxLayout()
        layout.addWidget(self.label)
        layout.addWidget(self.edit_filter)
        layout.addWidget(self.list)
        layout_browse = QHBoxLayout()
        layout_browse.addWidget(self.button_browse)
        layout_browse.addWidget(self.label_browse)
        layout.addLayout(layout_browse)
        layout.addSpacing(12)  # FIXME: Use metrics
        layout.addWidget(self.button_box)
        self.setLayout(layout)

        # Signals
        self.edit_filter.textChanged.connect(self.filter)
        self.button_browse.clicked.connect(lambda x: self.browse())
        self.button_ok.clicked.connect(self.accept)
        self.button_cancel.clicked.connect(self.reject)
        self.list.currentItemChanged.connect(self._refresh)

        self._refresh()
        self.setup()

    def setup(self, applications=None):
        """Load installed applications."""
        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        self.list.clear()
        if applications is None:
            apps = get_installed_applications()
        else:
            apps = applications

        for app in sorted(apps, key=lambda x: x.lower()):
            fpath = apps[app]
            icon = get_application_icon(fpath)
            item = QListWidgetItem(icon, app)
            item.setToolTip(fpath)
            item.fpath = fpath
            self.list.addItem(item)

        # FIXME: Use metrics
        self.list.setMinimumWidth(self.list.sizeHintForColumn(0) + 24)
        QApplication.restoreOverrideCursor()
        self._refresh()

    def _refresh(self):
        """Refresh the status of buttons on widget."""
        self.button_ok.setEnabled(self.list.currentRow() != -1)

    def browse(self, fpath=None):
        """Prompt user to select an application not found on the list."""
        app = None
        item = None

        if sys.platform == 'darwin':
            if fpath is None:
                basedir = '/Applications/'
                filters = _('Applications (*.app)')
                title = _('Select application')
                fpath, __ = getopenfilename(self, title, basedir, filters)

            if fpath and fpath.endswith('.app') and os.path.isdir(fpath):
                app = os.path.basename(fpath).split('.app')[0]
                for row in range(self.list.count()):
                    item = self.list.item(row)
                    if app == item.text() and fpath == item.fpath:
                        break
                else:
                    item = None
        elif os.name == 'nt':
            if fpath is None:
                basedir = 'C:\\'
                filters = _('Applications (*.exe *.bat *.com)')
                title = _('Select application')
                fpath, __ = getopenfilename(self, title, basedir, filters)

            if fpath:
                check_1 = fpath.endswith('.bat') and is_text_file(fpath)
                check_2 = (fpath.endswith(('.exe', '.com'))
                           and not is_text_file(fpath))
                if check_1 or check_2:
                    app = os.path.basename(fpath).capitalize().rsplit('.')[0]
                    for row in range(self.list.count()):
                        item = self.list.item(row)
                        if app == item.text() and fpath == item.fpath:
                            break
                    else:
                        item = None
        else:
            if fpath is None:
                basedir = '/'
                filters = _('Applications (*.desktop)')
                title = _('Select application')
                fpath, __ = getopenfilename(self, title, basedir, filters)

            if fpath and fpath.endswith(('.desktop')) and is_text_file(fpath):
                entry_data = parse_linux_desktop_entry(fpath)
                app = entry_data['name']
                for row in range(self.list.count()):
                    item = self.list.item(row)
                    if app == item.text() and fpath == item.fpath:
                        break
                else:
                    item = None

        if fpath:
            if item:
                self.list.setCurrentItem(item)
            elif app:
                icon = get_application_icon(fpath)
                item = QListWidgetItem(icon, app)
                item.fpath = fpath
                self.list.addItem(item)
                self.list.setCurrentItem(item)

        self.list.setFocus()
        self._refresh()

    def filter(self, text):
        """Filter the list of applications based on text."""
        text = self.edit_filter.text().lower().strip()
        for row in range(self.list.count()):
            item = self.list.item(row)
            item.setHidden(text not in item.text().lower())
        self._refresh()

    def set_extension(self, extension):
        """Set the extension on the label of the dialog."""
        self.label.setText(
            _('Choose the application for files of type ') + extension)

    @property
    def application_path(self):
        """Return the selected application path to executable."""
        item = self.list.currentItem()
        path = item.fpath if item else ''
        return path

    @property
    def application_name(self):
        """Return the selected application name."""
        item = self.list.currentItem()
        text = item.text() if item else ''
        return text
示例#16
0
class PreferencesDialog(QDialog):
    """Preferences Dialog for Napari user settings."""

    valueChanged = Signal()

    ui_schema = {
        "call_order": {
            "ui:widget": "plugins"
        },
        "highlight_thickness": {
            "ui:widget": "highlight"
        },
    }

    resized = Signal(QSize)
    closed = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)

        self._list = QListWidget(self)
        self._stack = QStackedWidget(self)

        self._list.setObjectName("Preferences")

        # Set up buttons
        self._button_cancel = QPushButton(trans._("Cancel"))
        self._button_ok = QPushButton(trans._("OK"))
        self._default_restore = QPushButton(trans._("Restore defaults"))

        # Setup
        self.setWindowTitle(trans._("Preferences"))

        # Layout
        left_layout = QVBoxLayout()
        left_layout.addWidget(self._list)
        left_layout.addStretch()
        left_layout.addWidget(self._default_restore)
        left_layout.addWidget(self._button_cancel)
        left_layout.addWidget(self._button_ok)

        main_layout = QHBoxLayout()
        main_layout.addLayout(left_layout, 1)
        main_layout.addWidget(self._stack, 3)

        self.setLayout(main_layout)

        # Signals

        self._list.currentRowChanged.connect(
            lambda index: self._stack.setCurrentIndex(index))
        self._button_cancel.clicked.connect(self.on_click_cancel)
        self._button_ok.clicked.connect(self.on_click_ok)
        self._default_restore.clicked.connect(self.restore_defaults)

        # Make widget

        self.make_dialog()
        self._list.setCurrentRow(0)

    def _restart_dialog(self, event=None, extra_str=""):
        """Displays the dialog informing user a restart is required.

        Paramters
        ---------
        event : Event
        extra_str : str
            Extra information to add to the message about needing a restart.
        """

        text_str = trans._(
            "napari requires a restart for image rendering changes to apply.")

        widget = ResetNapariInfoDialog(
            parent=self,
            text=text_str,
        )
        widget.exec_()

    def closeEvent(self, event):
        """Override to emit signal."""
        self.closed.emit()
        super().closeEvent(event)

    def reject(self):
        """Override to handle Escape."""
        super().reject()
        self.close()

    def resizeEvent(self, event):
        """Override to emit signal."""
        self.resized.emit(event.size())
        super().resizeEvent(event)

    def make_dialog(self):
        """Removes settings not to be exposed to user and creates dialog pages."""
        settings = get_settings()
        # Because there are multiple pages, need to keep a dictionary of values dicts.
        # One set of keywords are for each page, then in each entry for a page, there are dicts
        # of setting and its value.
        self._values_orig_dict = {}
        self._values_dict = {}
        self._setting_changed_dict = {}

        for page, setting in settings.schemas().items():
            schema, values, properties = self.get_page_dict(setting)

            self._setting_changed_dict[page] = {}
            self._values_orig_dict[page] = values
            self._values_dict[page] = values

            # Only add pages if there are any properties to add.
            if properties:
                self.add_page(schema, values)

    def get_page_dict(self, setting):
        """Provides the schema, set of values for each setting, and the properties
        for each setting.

        Parameters
        ----------
        setting : dict
            Dictionary of settings for a page within the settings manager.

        Returns
        -------
        schema : dict
            Json schema of the setting page.
        values : dict
            Dictionary of values currently set for each parameter in the settings.
        properties : dict
            Dictionary of properties within the json schema.

        """
        schema = json.loads(setting['json_schema'])

        # Resolve allOf references
        definitions = schema.get("definitions", {})
        if definitions:
            for key, data in schema["properties"].items():
                if "allOf" in data:
                    allof = data["allOf"]
                    allof = [d["$ref"].rsplit("/")[-1] for d in allof]
                    for definition in allof:
                        local_def = definitions[definition]
                        schema["properties"][key]["enum"] = local_def["enum"]
                        schema["properties"][key]["type"] = "string"

        # Need to remove certain properties that will not be displayed on the GUI
        properties = schema.pop('properties')
        model = setting['model']
        values = model.dict()
        napari_config = getattr(model, "NapariConfig", None)
        if napari_config is not None:
            for val in napari_config.preferences_exclude:
                properties.pop(val)
                values.pop(val)

        schema['properties'] = properties

        return schema, values, properties

    def restore_defaults(self):
        """Launches dialog to confirm restore settings choice."""
        self._reset_dialog = ConfirmDialog(
            parent=self,
            text=trans._("Are you sure you want to restore default settings?"),
        )
        self._reset_dialog.valueChanged.connect(self._reset_widgets)
        self._reset_dialog.exec_()

    def _reset_widgets(self):
        """Deletes the widgets and rebuilds with defaults."""
        self.close()
        self.valueChanged.emit()
        self._list.clear()

        for n in range(self._stack.count()):
            widget = self._stack.removeWidget(self._stack.currentWidget())
            del widget

        self.make_dialog()
        self._list.setCurrentRow(0)
        self.show()

    def on_click_ok(self):
        """Keeps the selected preferences saved to settings."""
        self.close()

    def on_click_cancel(self):
        """Restores the settings in place when dialog was launched."""
        # Need to check differences for each page.
        settings = get_settings()
        for n in range(self._stack.count()):
            # Must set the current row so that the proper list is updated
            # in check differences.
            self._list.setCurrentRow(n)
            page = self._list.currentItem().text().split(" ")[0].lower()
            # get new values for settings.  If they were changed from values at beginning
            # of preference dialog session, change them back.
            # Using the settings value seems to be the best way to get the checkboxes right
            # on the plugin call order widget.
            setting = settings.schemas()[page]
            schema, new_values, properties = self.get_page_dict(setting)
            self.check_differences(self._values_orig_dict[page], new_values)

        self._list.setCurrentRow(0)
        self.close()

    def add_page(self, schema, values):
        """Creates a new page for each section in dialog.

        Parameters
        ----------
        schema : dict
            Json schema including all information to build each page in the
            preferences dialog.
        values : dict
            Dictionary of current values set in preferences.
        """
        widget = self.build_page_dialog(schema, values)
        self._list.addItem(schema["title"])
        self._stack.addWidget(widget)

    def build_page_dialog(self, schema, values):
        """Builds the preferences widget using the json schema builder.

        Parameters
        ----------
        schema : dict
            Json schema including all information to build each page in the
            preferences dialog.
        values : dict
            Dictionary of current values set in preferences.
        """
        settings = get_settings()
        builder = WidgetBuilder()
        form = builder.create_form(schema, self.ui_schema)

        # Disable widgets that loaded settings from environment variables
        section = schema["section"]
        form_layout = form.widget.layout()
        for row in range(form.widget.layout().rowCount()):
            widget = form_layout.itemAt(row, form_layout.FieldRole).widget()
            name = widget._name
            disable = bool(
                settings._env_settings.get(section, {}).get(name, None))
            widget.setDisabled(disable)
            try:
                widget.opacity.setOpacity(0.3 if disable else 1)
            except AttributeError:
                # some widgets may not have opacity (such as the QtPluginSorter)
                pass

        # set state values for widget
        form.widget.state = values

        if section == 'experimental':
            # need to disable async if octree is enabled.
            if values['octree'] is True:
                form = self._disable_async(form, values)

        form.widget.on_changed.connect(lambda d: self.check_differences(
            d,
            self._values_dict[schema["title"].lower()],
        ))

        return form

    def _disable_async(self, form, values, disable=True, state=True):
        """Disable async if octree is True."""
        settings = get_settings()
        # need to make sure that if async_ is an environment setting, that we don't
        # enable it here.
        if (settings._env_settings['experimental'].get('async_', None)
                is not None):
            disable = True

        idx = list(values.keys()).index('async_')
        form_layout = form.widget.layout()
        widget = form_layout.itemAt(idx, form_layout.FieldRole).widget()
        widget.opacity.setOpacity(0.3 if disable else 1)
        widget.setDisabled(disable)

        return form

    def _values_changed(self, page, new_dict, old_dict):
        """Loops through each setting in a page to determine if it changed.

        Parameters
        ----------
        new_dict : dict
            Dict that has the most recent changes by user. Each key is a setting value
            and each item is the value.
        old_dict : dict
            Dict wtih values set at the begining of preferences dialog session.

        """
        for setting_name, value in new_dict.items():
            if value != old_dict[setting_name]:
                self._setting_changed_dict[page][setting_name] = value
            elif (value == old_dict[setting_name]
                  and setting_name in self._setting_changed_dict[page]):
                self._setting_changed_dict[page].pop(setting_name)

    def set_current_index(self, index: int):
        """
        Set the current page on the preferences by index.

        Parameters
        ----------
        index : int
            Index of page to set as current one.
        """
        self._list.setCurrentRow(index)

    def check_differences(self, new_dict, old_dict):
        """Changes settings in settings manager with changes from dialog.

        Parameters
        ----------
        new_dict : dict
            Dict that has the most recent changes by user. Each key is a setting parameter
            and each item is the value.
        old_dict : dict
            Dict wtih values set at the beginning of the preferences dialog session.
        """
        settings = get_settings()
        page = self._list.currentItem().text().split(" ")[0].lower()
        self._values_changed(page, new_dict, old_dict)
        different_values = self._setting_changed_dict[page]

        if len(different_values) > 0:
            # change the values in settings
            for setting_name, value in different_values.items():
                try:
                    setattr(settings._settings[page], setting_name, value)
                    self._values_dict[page] = new_dict

                    if page == 'experimental':

                        if setting_name == 'octree':

                            # disable/enable async checkbox
                            widget = self._stack.currentWidget()
                            cstate = True if value is True else False
                            self._disable_async(widget,
                                                new_dict,
                                                disable=cstate)

                            # need to inform user that napari restart needed.
                            self._restart_dialog()

                        elif setting_name == 'async_':
                            # need to inform user that napari restart needed.
                            self._restart_dialog()

                except:  # noqa: E722
                    continue
示例#17
0
class PreferencesDialog(QDialog):
    """Preferences Dialog for Napari user settings."""

    resized = Signal(QSize)

    def __init__(self, parent=None):
        super().__init__(parent)

        self._list = QListWidget(self)
        self._stack = QStackedWidget(self)

        self._list.setObjectName("Preferences")

        # Set up buttons
        self._button_cancel = QPushButton(trans._("Cancel"))
        self._button_ok = QPushButton(trans._("OK"))
        self._default_restore = QPushButton(trans._("Restore defaults"))

        # Setup
        self.setWindowTitle(trans._("Preferences"))

        # Layout
        left_layout = QVBoxLayout()
        left_layout.addWidget(self._list)
        left_layout.addStretch()
        left_layout.addWidget(self._default_restore)
        left_layout.addWidget(self._button_cancel)
        left_layout.addWidget(self._button_ok)

        main_layout = QHBoxLayout()
        main_layout.addLayout(left_layout, 1)
        main_layout.addWidget(self._stack, 3)

        self.setLayout(main_layout)

        # Signals

        self._list.currentRowChanged.connect(
            lambda index: self._stack.setCurrentIndex(index))
        self._button_cancel.clicked.connect(self.on_click_cancel)
        self._button_ok.clicked.connect(self.on_click_ok)
        self._default_restore.clicked.connect(self.restore_defaults)

        # Make widget

        self.make_dialog()
        self._list.setCurrentRow(0)

    def resizeEvent(self, event):
        """Override to emit signal."""
        self.resized.emit(event.size())
        super().resizeEvent(event)

    def make_dialog(self):
        """Removes settings not to be exposed to user and creates dialog pages."""
        # Because there are multiple pages, need to keep a list of values sets.
        self._values_orig_set_list = []
        self._values_set_list = []
        for _key, setting in SETTINGS.schemas().items():
            schema = json.loads(setting['json_schema'])
            # Need to remove certain properties that will not be displayed on the GUI
            properties = schema.pop('properties')
            model = setting['model']
            values = model.dict()
            napari_config = getattr(model, "NapariConfig", None)
            if napari_config is not None:
                for val in napari_config.preferences_exclude:
                    properties.pop(val)
                    values.pop(val)

            schema['properties'] = properties
            self._values_orig_set_list.append(set(values.items()))
            self._values_set_list.append(set(values.items()))

            # Only add pages if there are any properties to add.
            if properties:
                self.add_page(schema, values)

    def restore_defaults(self):
        """Launches dialog to confirm restore settings choice."""

        widget = ConfirmDialog(
            parent=self,
            text=trans._("Are you sure you want to restore default settings?"),
        )
        widget.valueChanged.connect(self._reset_widgets)
        widget.exec_()

    def _reset_widgets(self):
        """Deletes the widgets and rebuilds with defaults."""
        self.close()
        self._list.clear()

        for n in range(self._stack.count()):
            widget = self._stack.removeWidget(self._stack.currentWidget())
            del widget

        self.make_dialog()
        self._list.setCurrentRow(0)
        self.show()

    def on_click_ok(self):
        """Keeps the selected preferences saved to SETTINGS."""
        self.close()

    def on_click_cancel(self):
        """Restores the settings in place when dialog was launched."""
        # Need to check differences for each page.
        for n in range(self._stack.count()):
            # Must set the current row so that the proper set list is updated
            # in check differences.
            self._list.setCurrentRow(n)
            self.check_differences(
                self._values_orig_set_list[n],
                self._values_set_list[n],
            )
        self._list.setCurrentRow(0)
        self.close()

    def add_page(self, schema, values):
        """Creates a new page for each section in dialog.

        Parameters
        ----------
        schema : dict
            Json schema including all information to build each page in the
            preferences dialog.
        values : dict
            Dictionary of current values set in preferences.
        """
        widget = self.build_page_dialog(schema, values)
        self._list.addItem(schema["title"])
        self._stack.addWidget(widget)

    def build_page_dialog(self, schema, values):
        """Builds the preferences widget using the json schema builder.

        Parameters
        ----------
        schema : dict
            Json schema including all information to build each page in the
            preferences dialog.
        values : dict
            Dictionary of current values set in preferences.
        """

        builder = WidgetBuilder()
        form = builder.create_form(schema, {})
        # set state values for widget
        form.widget.state = values
        form.widget.on_changed.connect(lambda d: self.check_differences(
            set(d.items()),
            self._values_set_list[self._list.currentIndex().row()],
        ))

        return form

    def check_differences(self, new_set, values_set):
        """Changes settings in settings manager with changes from dialog.

        Parameters
        ----------
        new_set : set
            The set of new values, with tuples of key value pairs for each
            setting.
        values_set : set
            The old set of values.
        """

        page = self._list.currentItem().text().split(" ")[0].lower()
        different_values = list(new_set - values_set)

        if len(different_values) > 0:
            # change the values in SETTINGS
            for val in different_values:
                try:
                    setattr(SETTINGS._settings[page], val[0], val[1])
                    self._values_set_list[
                        self._list.currentIndex().row()] = new_set
                except:  # noqa: E722
                    continue
示例#18
0
class PathManager(QDialog):
    redirect_stdio = Signal(bool)

    def __init__(self,
                 parent=None,
                 pathlist=None,
                 ro_pathlist=None,
                 not_active_pathlist=None,
                 sync=True):
        QDialog.__init__(self, parent)

        # Destroying the C++ object right after closing the dialog box,
        # otherwise it may be garbage-collected in another QThread
        # (e.g. the editor's analysis thread in Spyder), thus leading to
        # a segmentation fault on UNIX or an application crash on Windows
        self.setAttribute(Qt.WA_DeleteOnClose)

        assert isinstance(pathlist, list)
        self.pathlist = pathlist
        if not_active_pathlist is None:
            not_active_pathlist = []
        self.not_active_pathlist = not_active_pathlist
        if ro_pathlist is None:
            ro_pathlist = []
        self.ro_pathlist = ro_pathlist

        self.last_path = getcwd_or_home()

        self.setWindowTitle(_("PYTHONPATH manager"))
        self.setWindowIcon(ima.icon('pythonpath'))
        self.resize(500, 300)

        self.selection_widgets = []

        layout = QVBoxLayout()
        self.setLayout(layout)

        top_layout = QHBoxLayout()
        layout.addLayout(top_layout)
        self.toolbar_widgets1 = self.setup_top_toolbar(top_layout)

        self.listwidget = QListWidget(self)
        self.listwidget.currentRowChanged.connect(self.refresh)
        self.listwidget.itemChanged.connect(self.update_not_active_pathlist)
        layout.addWidget(self.listwidget)

        bottom_layout = QHBoxLayout()
        layout.addLayout(bottom_layout)
        self.sync_button = None
        self.toolbar_widgets2 = self.setup_bottom_toolbar(bottom_layout, sync)

        # Buttons configuration
        bbox = QDialogButtonBox(QDialogButtonBox.Close)
        bbox.rejected.connect(self.reject)
        bottom_layout.addWidget(bbox)

        self.update_list()
        self.refresh()

    @property
    def active_pathlist(self):
        return [
            path for path in self.pathlist
            if path not in self.not_active_pathlist
        ]

    def _add_widgets_to_layout(self, layout, widgets):
        layout.setAlignment(Qt.AlignLeft)
        for widget in widgets:
            layout.addWidget(widget)

    def setup_top_toolbar(self, layout):
        toolbar = []
        movetop_button = create_toolbutton(
            self,
            text=_("Move to top"),
            icon=ima.icon('2uparrow'),
            triggered=lambda: self.move_to(absolute=0),
            text_beside_icon=True)
        toolbar.append(movetop_button)
        moveup_button = create_toolbutton(
            self,
            text=_("Move up"),
            icon=ima.icon('1uparrow'),
            triggered=lambda: self.move_to(relative=-1),
            text_beside_icon=True)
        toolbar.append(moveup_button)
        movedown_button = create_toolbutton(
            self,
            text=_("Move down"),
            icon=ima.icon('1downarrow'),
            triggered=lambda: self.move_to(relative=1),
            text_beside_icon=True)
        toolbar.append(movedown_button)
        movebottom_button = create_toolbutton(
            self,
            text=_("Move to bottom"),
            icon=ima.icon('2downarrow'),
            triggered=lambda: self.move_to(absolute=1),
            text_beside_icon=True)
        toolbar.append(movebottom_button)
        self.selection_widgets.extend(toolbar)
        self._add_widgets_to_layout(layout, toolbar)
        return toolbar

    def setup_bottom_toolbar(self, layout, sync=True):
        toolbar = []
        add_button = create_toolbutton(self,
                                       text=_('Add path'),
                                       icon=ima.icon('edit_add'),
                                       triggered=self.add_path,
                                       text_beside_icon=True)
        toolbar.append(add_button)
        remove_button = create_toolbutton(self,
                                          text=_('Remove path'),
                                          icon=ima.icon('edit_remove'),
                                          triggered=self.remove_path,
                                          text_beside_icon=True)
        toolbar.append(remove_button)
        self.selection_widgets.append(remove_button)
        self._add_widgets_to_layout(layout, toolbar)
        layout.addStretch(1)
        if os.name == 'nt' and sync:
            self.sync_button = create_toolbutton(
                self,
                text=_("Synchronize..."),
                icon=ima.icon('fileimport'),
                triggered=self.synchronize,
                tip=_("Synchronize Spyder's path list with PYTHONPATH "
                      "environment variable"),
                text_beside_icon=True)
            layout.addWidget(self.sync_button)
        return toolbar

    @Slot()
    def synchronize(self):
        """
        Synchronize Spyder's path list with PYTHONPATH environment variable
        Only apply to: current user, on Windows platforms
        """
        answer = QMessageBox.question(
            self, _("Synchronize"),
            _("This will synchronize Spyder's path list with "
              "<b>PYTHONPATH</b> environment variable for current user, "
              "allowing you to run your Python modules outside Spyder "
              "without having to configure sys.path. "
              "<br>Do you want to clear contents of PYTHONPATH before "
              "adding Spyder's path list?"),
            QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
        if answer == QMessageBox.Cancel:
            return
        elif answer == QMessageBox.Yes:
            remove = True
        else:
            remove = False
        from spyder.utils.environ import (get_user_env, set_user_env,
                                          listdict2envdict)
        env = get_user_env()
        if remove:
            ppath = self.active_pathlist + self.ro_pathlist
        else:
            ppath = env.get('PYTHONPATH', [])
            if not isinstance(ppath, list):
                ppath = [ppath]
            ppath = [
                path for path in ppath
                if path not in (self.active_pathlist + self.ro_pathlist)
            ]
            ppath.extend(self.active_pathlist + self.ro_pathlist)
        env['PYTHONPATH'] = ppath
        set_user_env(listdict2envdict(env), parent=self)

    def get_path_list(self):
        """Return path list (does not include the read-only path list)"""
        return self.pathlist

    def update_not_active_pathlist(self, item):
        path = item.text()
        if bool(item.checkState()) is True:
            self.remove_from_not_active_pathlist(path)
        else:
            self.add_to_not_active_pathlist(path)

    def add_to_not_active_pathlist(self, path):
        if path not in self.not_active_pathlist:
            self.not_active_pathlist.append(path)

    def remove_from_not_active_pathlist(self, path):
        if path in self.not_active_pathlist:
            self.not_active_pathlist.remove(path)

    def update_list(self):
        """Update path list"""
        self.listwidget.clear()
        for name in self.pathlist + self.ro_pathlist:
            item = QListWidgetItem(name)
            item.setIcon(ima.icon('DirClosedIcon'))
            if name in self.ro_pathlist:
                item.setFlags(Qt.NoItemFlags | Qt.ItemIsUserCheckable)
                item.setCheckState(Qt.Checked)
            elif name in self.not_active_pathlist:
                item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
                item.setCheckState(Qt.Unchecked)
            else:
                item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
                item.setCheckState(Qt.Checked)
            self.listwidget.addItem(item)
        self.refresh()

    def refresh(self, row=None):
        """Refresh widget"""
        for widget in self.selection_widgets:
            widget.setEnabled(self.listwidget.currentItem() is not None)
        not_empty = self.listwidget.count() > 0
        if self.sync_button is not None:
            self.sync_button.setEnabled(not_empty)

    def move_to(self, absolute=None, relative=None):
        index = self.listwidget.currentRow()
        if absolute is not None:
            if absolute:
                new_index = len(self.pathlist) - 1
            else:
                new_index = 0
        else:
            new_index = index + relative
        new_index = max(0, min(len(self.pathlist) - 1, new_index))
        path = self.pathlist.pop(index)
        self.pathlist.insert(new_index, path)
        self.update_list()
        self.listwidget.setCurrentRow(new_index)

    @Slot()
    def remove_path(self):
        answer = QMessageBox.warning(
            self, _("Remove path"),
            _("Do you really want to remove selected path?"),
            QMessageBox.Yes | QMessageBox.No)
        if answer == QMessageBox.Yes:
            self.pathlist.pop(self.listwidget.currentRow())
            self.remove_from_not_active_pathlist(
                self.listwidget.currentItem().text())
            self.update_list()

    @Slot()
    def add_path(self):
        self.redirect_stdio.emit(False)
        directory = getexistingdirectory(self, _("Select directory"),
                                         self.last_path)
        self.redirect_stdio.emit(True)
        if directory:
            is_unicode = False
            if PY2:
                try:
                    directory.decode('ascii')
                except UnicodeEncodeError:
                    is_unicode = True
                if is_unicode:
                    QMessageBox.warning(
                        self, _("Add path"),
                        _("You are using Python 2 and the path"
                          " selected has Unicode characters. "
                          "The new path will not be added."), QMessageBox.Ok)
                    return
            directory = osp.abspath(directory)
            self.last_path = directory
            if directory in self.pathlist:
                item = self.listwidget.findItems(directory, Qt.MatchExactly)[0]
                item.setCheckState(Qt.Checked)
                answer = QMessageBox.question(
                    self, _("Add path"),
                    _("This directory is already included in Spyder "
                      "path list.<br>Do you want to move it to the "
                      "top of the list?"), QMessageBox.Yes | QMessageBox.No)
                if answer == QMessageBox.Yes:
                    self.pathlist.remove(directory)
                else:
                    return
            self.pathlist.insert(0, directory)
            self.update_list()
示例#19
0
class PathManager(QDialog):
    """Path manager dialog."""
    redirect_stdio = Signal(bool)
    sig_path_changed = Signal(object)

    def __init__(self,
                 parent,
                 path=None,
                 read_only_path=None,
                 not_active_path=None,
                 sync=True):
        """Path manager dialog."""
        super(PathManager, self).__init__(parent)
        assert isinstance(path, (tuple, None))

        self.path = path or ()
        self.read_only_path = read_only_path or ()
        self.not_active_path = not_active_path or ()
        self.last_path = getcwd_or_home()
        self.original_path_dict = None

        # Widgets
        self.add_button = None
        self.remove_button = None
        self.movetop_button = None
        self.moveup_button = None
        self.movedown_button = None
        self.movebottom_button = None
        self.sync_button = None
        self.selection_widgets = []
        self.top_toolbar_widgets = self._setup_top_toolbar()
        self.bottom_toolbar_widgets = self._setup_bottom_toolbar()
        self.listwidget = QListWidget(self)
        self.bbox = QDialogButtonBox(QDialogButtonBox.Ok
                                     | QDialogButtonBox.Cancel)
        self.button_ok = self.bbox.button(QDialogButtonBox.Ok)

        # Widget setup
        # Destroying the C++ object right after closing the dialog box,
        # otherwise it may be garbage-collected in another QThread
        # (e.g. the editor's analysis thread in Spyder), thus leading to
        # a segmentation fault on UNIX or an application crash on Windows
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.setWindowTitle(_("PYTHONPATH manager"))
        self.setWindowIcon(ima.icon('pythonpath'))
        self.resize(500, 300)
        self.sync_button.setVisible(os.name == 'nt' and sync)

        # Layouts
        top_layout = QHBoxLayout()
        self._add_widgets_to_layout(self.top_toolbar_widgets, top_layout)

        bottom_layout = QHBoxLayout()
        self._add_widgets_to_layout(self.bottom_toolbar_widgets, bottom_layout)
        bottom_layout.addWidget(self.bbox)

        layout = QVBoxLayout()
        layout.addLayout(top_layout)
        layout.addWidget(self.listwidget)
        layout.addLayout(bottom_layout)
        self.setLayout(layout)

        # Signals
        self.listwidget.currentRowChanged.connect(lambda x: self.refresh())
        self.listwidget.itemChanged.connect(lambda x: self.refresh())
        self.bbox.accepted.connect(self.accept)
        self.bbox.rejected.connect(self.reject)

        # Setup
        self.setup()

    def _add_widgets_to_layout(self, widgets, layout):
        """Helper to add toolbar widgets to top and bottom layout."""
        layout.setAlignment(Qt.AlignLeft)
        for widget in widgets:
            if widget is None:
                layout.addStretch(1)
            else:
                layout.addWidget(widget)

    def _setup_top_toolbar(self):
        """Create top toolbar and actions."""
        self.movetop_button = create_toolbutton(
            self,
            text=_("Move to top"),
            icon=ima.icon('2uparrow'),
            triggered=lambda: self.move_to(absolute=0),
            text_beside_icon=True)
        self.moveup_button = create_toolbutton(
            self,
            text=_("Move up"),
            icon=ima.icon('1uparrow'),
            triggered=lambda: self.move_to(relative=-1),
            text_beside_icon=True)
        self.movedown_button = create_toolbutton(
            self,
            text=_("Move down"),
            icon=ima.icon('1downarrow'),
            triggered=lambda: self.move_to(relative=1),
            text_beside_icon=True)
        self.movebottom_button = create_toolbutton(
            self,
            text=_("Move to bottom"),
            icon=ima.icon('2downarrow'),
            triggered=lambda: self.move_to(absolute=1),
            text_beside_icon=True)

        toolbar = [
            self.movetop_button, self.moveup_button, self.movedown_button,
            self.movebottom_button
        ]
        self.selection_widgets.extend(toolbar)
        return toolbar

    def _setup_bottom_toolbar(self):
        """Create bottom toolbar and actions."""
        self.add_button = create_toolbutton(
            self,
            text=_('Add path'),
            icon=ima.icon('edit_add'),
            triggered=lambda x: self.add_path(),
            text_beside_icon=True)
        self.remove_button = create_toolbutton(
            self,
            text=_('Remove path'),
            icon=ima.icon('edit_remove'),
            triggered=lambda x: self.remove_path(),
            text_beside_icon=True)
        self.sync_button = create_toolbutton(
            self,
            text=_("Synchronize..."),
            icon=ima.icon('fileimport'),
            triggered=self.synchronize,
            tip=_("Synchronize Spyder's path list with PYTHONPATH "
                  "environment variable"),
            text_beside_icon=True)

        self.selection_widgets.append(self.remove_button)
        return [self.add_button, self.remove_button, None, self.sync_button]

    def _create_item(self, path):
        """Helper to create a new list item."""
        item = QListWidgetItem(path)
        item.setIcon(ima.icon('DirClosedIcon'))

        if path in self.read_only_path:
            item.setFlags(Qt.NoItemFlags | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Checked)
        elif path in self.not_active_path:
            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Unchecked)
        else:
            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Checked)

        return item

    @property
    def editable_bottom_row(self):
        """Maximum bottom row count that is editable."""
        read_only_count = len(self.read_only_path)
        if read_only_count == 0:
            max_row = self.listwidget.count() - 1
        else:
            max_row = self.listwidget.count() - read_only_count - 1
        return max_row

    def setup(self):
        """Populate list widget."""
        self.listwidget.clear()
        for path in self.path + self.read_only_path:
            item = self._create_item(path)
            self.listwidget.addItem(item)
        self.listwidget.setCurrentRow(0)
        self.original_path_dict = self.get_path_dict()
        self.refresh()

    @Slot()
    def synchronize(self):
        """
        Synchronize Spyder's path list with PYTHONPATH environment variable
        Only apply to: current user, on Windows platforms.
        """
        answer = QMessageBox.question(
            self, _("Synchronize"),
            _("This will synchronize Spyder's path list with "
              "<b>PYTHONPATH</b> environment variable for the current user, "
              "allowing you to run your Python modules outside Spyder "
              "without having to configure sys.path. "
              "<br>"
              "Do you want to clear contents of PYTHONPATH before "
              "adding Spyder's path list?"),
            QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)

        if answer == QMessageBox.Cancel:
            return
        elif answer == QMessageBox.Yes:
            remove = True
        else:
            remove = False

        from spyder.utils.environ import (get_user_env, listdict2envdict,
                                          set_user_env)
        env = get_user_env()

        # Includes read only paths
        active_path = tuple(k for k, v in self.get_path_dict(True).items()
                            if v)

        if remove:
            ppath = active_path
        else:
            ppath = env.get('PYTHONPATH', [])
            if not isinstance(ppath, list):
                ppath = [ppath]

            ppath = tuple(p for p in ppath if p not in active_path)
            ppath = ppath + active_path

        env['PYTHONPATH'] = list(ppath)
        set_user_env(listdict2envdict(env), parent=self)

    def get_path_dict(self, read_only=False):
        """
        Return an ordered dict with the path entries as keys and the active
        state as the value.

        If `read_only` is True, the read_only entries are also included.
        `read_only` entry refers to the project path entry.
        """
        odict = OrderedDict()
        for row in range(self.listwidget.count()):
            item = self.listwidget.item(row)
            path = item.text()
            if path in self.read_only_path and not read_only:
                continue
            odict[path] = item.checkState() == Qt.Checked
        return odict

    def refresh(self):
        """Refresh toolbar widgets."""
        enabled = self.listwidget.currentItem() is not None
        for widget in self.selection_widgets:
            widget.setEnabled(enabled)

        # Disable buttons based on row
        row = self.listwidget.currentRow()
        disable_widgets = []

        # Move up/top disabled for top item
        if row == 0:
            disable_widgets.extend([self.movetop_button, self.moveup_button])

        # Move down/bottom disabled for bottom item
        if row == self.editable_bottom_row:
            disable_widgets.extend(
                [self.movebottom_button, self.movedown_button])
        for widget in disable_widgets:
            widget.setEnabled(False)

        self.sync_button.setEnabled(self.listwidget.count() > 0)

        # Ok button only enabled if actual changes occur
        self.button_ok.setEnabled(
            self.original_path_dict != self.get_path_dict())

    def check_path(self, path):
        """Check that the path is not a [site|dist]-packages folder."""
        if os.name == 'nt':
            pat = re.compile(r'.*lib/(?:site|dist)-packages.*')
        else:
            pat = re.compile(r'.*lib/python.../(?:site|dist)-packages.*')

        path_norm = path.replace('\\', '/')
        return pat.match(path_norm) is None

    @Slot()
    def add_path(self, directory=None):
        """
        Add path to list widget.

        If `directory` is provided, the folder dialog is overridden.
        """
        if directory is None:
            self.redirect_stdio.emit(False)
            directory = getexistingdirectory(self, _("Select directory"),
                                             self.last_path)
            self.redirect_stdio.emit(True)

        if PY2:
            is_unicode = False
            try:
                directory.decode('ascii')
            except (UnicodeEncodeError, UnicodeDecodeError):
                is_unicode = True

            if is_unicode:
                QMessageBox.warning(
                    self, _("Add path"),
                    _("You are using Python 2 and the selected path has "
                      "Unicode characters."
                      "<br> "
                      "Therefore, this path will not be added."),
                    QMessageBox.Ok)
                return

        directory = osp.abspath(directory)
        self.last_path = directory

        if directory in self.get_path_dict():
            item = self.listwidget.findItems(directory, Qt.MatchExactly)[0]
            item.setCheckState(Qt.Checked)
            answer = QMessageBox.question(
                self, _("Add path"),
                _("This directory is already included in the list."
                  "<br> "
                  "Do you want to move it to the top of it?"),
                QMessageBox.Yes | QMessageBox.No)

            if answer == QMessageBox.Yes:
                item = self.listwidget.takeItem(self.listwidget.row(item))
                self.listwidget.insertItem(0, item)
                self.listwidget.setCurrentRow(0)
        else:
            if self.check_path(directory):
                item = self._create_item(directory)
                self.listwidget.insertItem(0, item)
                self.listwidget.setCurrentRow(0)
            else:
                answer = QMessageBox.warning(
                    self, _("Add path"),
                    _("This directory cannot be added to the path!"
                      "<br><br>"
                      "If you want to set a different Python interpreter, "
                      "please go to <tt>Preferences > Main interpreter</tt>"
                      "."), QMessageBox.Ok)

        self.refresh()

    @Slot()
    def remove_path(self, force=False):
        """
        Remove path from list widget.

        If `force` is True, the message box is overridden.
        """
        if self.listwidget.currentItem():
            if not force:
                answer = QMessageBox.warning(
                    self, _("Remove path"),
                    _("Do you really want to remove the selected path?"),
                    QMessageBox.Yes | QMessageBox.No)

            if force or answer == QMessageBox.Yes:
                self.listwidget.takeItem(self.listwidget.currentRow())
                self.refresh()

    def move_to(self, absolute=None, relative=None):
        """Move items of list widget."""
        index = self.listwidget.currentRow()
        if absolute is not None:
            if absolute:
                new_index = self.listwidget.count() - 1
            else:
                new_index = 0
        else:
            new_index = index + relative

        new_index = max(0, min(self.editable_bottom_row, new_index))
        item = self.listwidget.takeItem(index)
        self.listwidget.insertItem(new_index, item)
        self.listwidget.setCurrentRow(new_index)
        self.refresh()

    def current_row(self):
        """Returns the current row of the list."""
        return self.listwidget.currentRow()

    def set_current_row(self, row):
        """Set the current row of the list."""
        self.listwidget.setCurrentRow(row)

    def row_check_state(self, row):
        """Return the checked state for item in row."""
        item = self.listwidget.item(row)
        return item.checkState()

    def set_row_check_state(self, row, value):
        """Set the current checked state for item in row."""
        item = self.listwidget.item(row)
        item.setCheckState(value)

    def count(self):
        """Return the number of items."""
        return self.listwidget.count()

    def accept(self):
        """Override Qt method."""
        path_dict = self.get_path_dict()
        if self.original_path_dict != path_dict:
            self.sig_path_changed.emit(path_dict)
        super(PathManager, self).accept()