|
27 | 27 |
|
28 | 28 | import codecs
|
29 | 29 | import sys
|
| 30 | +import operator |
30 | 31 | import os
|
31 | 32 | import math
|
32 | 33 |
|
33 | 34 | from qgis.PyQt import uic
|
34 |
| -from qgis.PyQt.QtCore import Qt, QRectF, QMimeData, QPoint, QPointF, QByteArray, QSize, QSizeF, pyqtSignal |
| 35 | +from qgis.PyQt.QtCore import Qt, QCoreApplication, QRectF, QMimeData, QPoint, QPointF, QByteArray, QSize, QSizeF, pyqtSignal |
35 | 36 | from qgis.PyQt.QtWidgets import QGraphicsView, QTreeWidget, QMessageBox, QFileDialog, QTreeWidgetItem, QSizePolicy, QMainWindow, QShortcut
|
36 | 37 | from qgis.PyQt.QtGui import QIcon, QImage, QPainter, QKeySequence
|
37 | 38 | from qgis.PyQt.QtSvg import QSvgGenerator
|
|
62 | 63 |
|
63 | 64 |
|
64 | 65 | class ModelerDialog(BASE, WIDGET):
|
| 66 | + ALG_ITEM = 'ALG_ITEM' |
| 67 | + PROVIDER_ITEM = 'PROVIDER_ITEM' |
| 68 | + GROUP_ITEM = 'GROUP_ITEM' |
| 69 | + |
| 70 | + NAME_ROLE = Qt.UserRole |
| 71 | + TAG_ROLE = Qt.UserRole + 1 |
| 72 | + TYPE_ROLE = Qt.UserRole + 2 |
65 | 73 |
|
66 | 74 | CANVAS_SIZE = 4000
|
67 | 75 |
|
@@ -239,7 +247,7 @@ def _mimeDataAlgorithm(items):
|
239 | 247 |
|
240 | 248 | # Connect signals and slots
|
241 | 249 | self.inputsTree.doubleClicked.connect(self.addInput)
|
242 |
| - self.searchBox.textChanged.connect(self.fillAlgorithmTree) |
| 250 | + self.searchBox.textChanged.connect(self.textChanged) |
243 | 251 | self.algorithmTree.doubleClicked.connect(self.addAlgorithm)
|
244 | 252 |
|
245 | 253 | # Ctrl+= should also trigger a zoom in action
|
@@ -272,7 +280,7 @@ def _mimeDataAlgorithm(items):
|
272 | 280 | self.model.setProvider(QgsApplication.processingRegistry().providerById('model'))
|
273 | 281 |
|
274 | 282 | self.fillInputsTree()
|
275 |
| - self.fillAlgorithmTree() |
| 283 | + self.fillTreeUsingProviders() |
276 | 284 |
|
277 | 285 | self.view.centerOn(0, 0)
|
278 | 286 | self.help = None
|
@@ -563,6 +571,49 @@ def getPositionForParameterItem(self):
|
563 | 571 | newX = MARGIN + BOX_WIDTH / 2
|
564 | 572 | return QPointF(newX, MARGIN + BOX_HEIGHT / 2)
|
565 | 573 |
|
| 574 | + def textChanged(self): |
| 575 | + text = self.searchBox.text().strip(' ').lower() |
| 576 | + for item in list(self.disabledProviderItems.values()): |
| 577 | + item.setHidden(True) |
| 578 | + self._filterItem(self.algorithmTree.invisibleRootItem(), [t for t in text.split(' ') if t]) |
| 579 | + if text: |
| 580 | + self.algorithmTree.expandAll() |
| 581 | + self.disabledWithMatchingAlgs = [] |
| 582 | + for provider in QgsApplication.processingRegistry().providers(): |
| 583 | + if not provider.isActive(): |
| 584 | + for alg in provider.algorithms(): |
| 585 | + if text in alg.name(): |
| 586 | + self.disabledWithMatchingAlgs.append(provider.id()) |
| 587 | + break |
| 588 | + else: |
| 589 | + self.algorithmTree.collapseAll() |
| 590 | + |
| 591 | + def _filterItem(self, item, text): |
| 592 | + if (item.childCount() > 0): |
| 593 | + show = False |
| 594 | + for i in range(item.childCount()): |
| 595 | + child = item.child(i) |
| 596 | + showChild = self._filterItem(child, text) |
| 597 | + show = (showChild or show) and item not in list(self.disabledProviderItems.values()) |
| 598 | + item.setHidden(not show) |
| 599 | + return show |
| 600 | + elif isinstance(item, (TreeAlgorithmItem, TreeActionItem)): |
| 601 | + # hide if every part of text is not contained somewhere in either the item text or item user role |
| 602 | + item_text = [item.text(0).lower(), item.data(0, ModelerDialog.NAME_ROLE).lower()] |
| 603 | + if isinstance(item, TreeAlgorithmItem): |
| 604 | + item_text.append(item.alg.id()) |
| 605 | + item_text.extend(item.data(0, ModelerDialog.TAG_ROLE)) |
| 606 | + |
| 607 | + hide = bool(text) and not all( |
| 608 | + any(part in t for t in item_text) |
| 609 | + for part in text) |
| 610 | + |
| 611 | + item.setHidden(hide) |
| 612 | + return not hide |
| 613 | + else: |
| 614 | + item.setHidden(True) |
| 615 | + return False |
| 616 | + |
566 | 617 | def fillInputsTree(self):
|
567 | 618 | icon = QIcon(os.path.join(pluginPath, 'images', 'input.svg'))
|
568 | 619 | parametersItem = QTreeWidgetItem()
|
@@ -619,88 +670,157 @@ def getPositionForAlgorithmItem(self):
|
619 | 670 | newY = MARGIN * 2 + BOX_HEIGHT + BOX_HEIGHT / 2
|
620 | 671 | return QPointF(newX, newY)
|
621 | 672 |
|
622 |
| - def fillAlgorithmTree(self): |
623 |
| - self.fillAlgorithmTreeUsingProviders() |
624 |
| - self.algorithmTree.sortItems(0, Qt.AscendingOrder) |
| 673 | + def fillTreeUsingProviders(self): |
| 674 | + self.algorithmTree.clear() |
| 675 | + self.disabledProviderItems = {} |
625 | 676 |
|
626 |
| - text = str(self.searchBox.text()) |
627 |
| - if text != '': |
628 |
| - self.algorithmTree.expandAll() |
| 677 | + # TODO - replace with proper model for toolbox! |
629 | 678 |
|
630 |
| - def fillAlgorithmTreeUsingProviders(self): |
631 |
| - self.algorithmTree.clear() |
632 |
| - text = str(self.searchBox.text()) |
633 |
| - search_strings = text.split(' ') |
634 |
| - qgis_groups = {} |
| 679 | + # first add qgis/native providers, since they create top level groups |
635 | 680 | for provider in QgsApplication.processingRegistry().providers():
|
636 |
| - if not provider.isActive(): |
| 681 | + if provider.id() in ('qgis', 'native', '3d'): |
| 682 | + self.addAlgorithmsFromProvider(provider, self.algorithmTree.invisibleRootItem()) |
| 683 | + else: |
637 | 684 | continue
|
638 |
| - groups = {} |
639 |
| - |
640 |
| - # Add algorithms |
641 |
| - for alg in provider.algorithms(): |
642 |
| - if alg.flags() & QgsProcessingAlgorithm.FlagHideFromModeler: |
643 |
| - continue |
644 |
| - if alg.id() == self.model.id(): |
645 |
| - continue |
646 |
| - |
647 |
| - item_text = [alg.displayName().lower()] |
648 |
| - item_text.extend(alg.tags()) |
649 |
| - |
650 |
| - show = not search_strings or all( |
651 |
| - any(part in t for t in item_text) |
652 |
| - for part in search_strings) |
653 |
| - |
654 |
| - if show: |
655 |
| - if alg.group() in groups: |
656 |
| - groupItem = groups[alg.group()] |
657 |
| - elif provider.id() in ('qgis', 'native', '3d') and alg.group() in qgis_groups: |
658 |
| - groupItem = qgis_groups[alg.group()] |
659 |
| - else: |
660 |
| - groupItem = QTreeWidgetItem() |
661 |
| - name = alg.group() |
662 |
| - groupItem.setText(0, name) |
663 |
| - groupItem.setToolTip(0, name) |
664 |
| - groupItem.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) |
665 |
| - if provider.id() in ('qgis', 'native', '3d'): |
666 |
| - groupItem.setIcon(0, provider.icon()) |
667 |
| - qgis_groups[alg.group()] = groupItem |
668 |
| - else: |
669 |
| - groups[alg.group()] = groupItem |
670 |
| - algItem = TreeAlgorithmItem(alg) |
671 |
| - groupItem.addChild(algItem) |
672 |
| - |
673 |
| - if len(groups) > 0: |
674 |
| - providerItem = QTreeWidgetItem() |
675 |
| - providerItem.setText(0, provider.name()) |
676 |
| - providerItem.setToolTip(0, provider.name()) |
677 |
| - providerItem.setIcon(0, provider.icon()) |
678 |
| - providerItem.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) |
679 |
| - for groupItem in list(groups.values()): |
680 |
| - providerItem.addChild(groupItem) |
681 |
| - self.algorithmTree.addTopLevelItem(providerItem) |
682 |
| - providerItem.setExpanded(text != '') |
683 |
| - for groupItem in list(groups.values()): |
684 |
| - if text != '': |
685 |
| - groupItem.setExpanded(True) |
686 |
| - |
687 |
| - if len(qgis_groups) > 0: |
688 |
| - for groupItem in list(qgis_groups.values()): |
689 |
| - self.algorithmTree.addTopLevelItem(groupItem) |
690 |
| - for groupItem in list(qgis_groups.values()): |
691 |
| - if text != '': |
692 |
| - groupItem.setExpanded(True) |
693 |
| - |
694 | 685 | self.algorithmTree.sortItems(0, Qt.AscendingOrder)
|
695 | 686 |
|
| 687 | + for provider in QgsApplication.processingRegistry().providers(): |
| 688 | + if provider.id() in ('qgis', 'native', '3d'): |
| 689 | + # already added |
| 690 | + continue |
| 691 | + else: |
| 692 | + providerItem = TreeProviderItem(provider, self.algorithmTree, self) |
| 693 | + |
| 694 | + if not provider.isActive(): |
| 695 | + providerItem.setHidden(True) |
| 696 | + self.disabledProviderItems[provider.id()] = providerItem |
| 697 | + |
| 698 | + # insert non-native providers at end of tree, alphabetically |
| 699 | + |
| 700 | + for i in range(self.algorithmTree.invisibleRootItem().childCount()): |
| 701 | + child = self.algorithmTree.invisibleRootItem().child(i) |
| 702 | + if isinstance(child, TreeProviderItem): |
| 703 | + if child.text(0) > providerItem.text(0): |
| 704 | + break |
| 705 | + |
| 706 | + self.algorithmTree.insertTopLevelItem(i + 1, providerItem) |
| 707 | + |
| 708 | + def addAlgorithmsFromProvider(self, provider, parent): |
| 709 | + groups = {} |
| 710 | + count = 0 |
| 711 | + algs = provider.algorithms() |
| 712 | + active = provider.isActive() |
| 713 | + |
| 714 | + # Add algorithms |
| 715 | + for alg in algs: |
| 716 | + if alg.flags() & QgsProcessingAlgorithm.FlagHideFromToolbox: |
| 717 | + continue |
| 718 | + groupItem = None |
| 719 | + if alg.group() in groups: |
| 720 | + groupItem = groups[alg.group()] |
| 721 | + else: |
| 722 | + # check if group already exists |
| 723 | + for i in range(parent.childCount()): |
| 724 | + if parent.child(i).text(0) == alg.group(): |
| 725 | + groupItem = parent.child(i) |
| 726 | + groups[alg.group()] = groupItem |
| 727 | + break |
| 728 | + |
| 729 | + if not groupItem: |
| 730 | + groupItem = TreeGroupItem(alg.group()) |
| 731 | + if not active: |
| 732 | + groupItem.setInactive() |
| 733 | + if provider.id() in ('qgis', 'native', '3d'): |
| 734 | + groupItem.setIcon(0, provider.icon()) |
| 735 | + groups[alg.group()] = groupItem |
| 736 | + algItem = TreeAlgorithmItem(alg) |
| 737 | + if not active: |
| 738 | + algItem.setForeground(0, Qt.darkGray) |
| 739 | + groupItem.addChild(algItem) |
| 740 | + count += 1 |
| 741 | + |
| 742 | + text = provider.name() |
| 743 | + |
| 744 | + if not provider.id() in ('qgis', 'native', '3d'): |
| 745 | + if not active: |
| 746 | + def activateProvider(): |
| 747 | + self.activateProvider(provider.id()) |
| 748 | + |
| 749 | + label = QLabel(text + " <a href='%s'>Activate</a>") |
| 750 | + label.setStyleSheet("QLabel {background-color: white; color: grey;}") |
| 751 | + label.linkActivated.connect(activateProvider) |
| 752 | + self.algorithmTree.setItemWidget(parent, 0, label) |
| 753 | + else: |
| 754 | + parent.setText(0, text) |
| 755 | + |
| 756 | + for group, groupItem in sorted(groups.items(), key=operator.itemgetter(1)): |
| 757 | + parent.addChild(groupItem) |
| 758 | + |
| 759 | + if not provider.id() in ('qgis', 'native', '3d'): |
| 760 | + parent.setHidden(parent.childCount() == 0) |
| 761 | + |
696 | 762 |
|
697 | 763 | class TreeAlgorithmItem(QTreeWidgetItem):
|
698 | 764 |
|
699 | 765 | def __init__(self, alg):
|
700 | 766 | QTreeWidgetItem.__init__(self)
|
701 | 767 | self.alg = alg
|
702 | 768 | icon = alg.icon()
|
| 769 | + nameEn = alg.name() |
703 | 770 | name = alg.displayName()
|
| 771 | + name = name if name != '' else nameEn |
704 | 772 | self.setIcon(0, icon)
|
| 773 | + self.setToolTip(0, self.formatAlgorithmTooltip(alg)) |
| 774 | + self.setText(0, name) |
| 775 | + self.setData(0, ModelerDialog.NAME_ROLE, nameEn) |
| 776 | + self.setData(0, ModelerDialog.TAG_ROLE, alg.tags()) |
| 777 | + self.setData(0, ModelerDialog.TYPE_ROLE, ModelerDialog.ALG_ITEM) |
| 778 | + |
| 779 | + def formatAlgorithmTooltip(self, alg): |
| 780 | + return '<p><b>{}</b></p><p>{}</p>'.format( |
| 781 | + alg.displayName(), |
| 782 | + QCoreApplication.translate('Toolbox', 'Algorithm ID: ‘{}’').format('<i>{}</i>'.format(alg.id())) |
| 783 | + ) |
| 784 | + |
| 785 | + |
| 786 | +class TreeGroupItem(QTreeWidgetItem): |
| 787 | + |
| 788 | + def __init__(self, name): |
| 789 | + QTreeWidgetItem.__init__(self) |
705 | 790 | self.setToolTip(0, name)
|
706 | 791 | self.setText(0, name)
|
| 792 | + self.setData(0, ModelerDialog.NAME_ROLE, name) |
| 793 | + self.setData(0, ModelerDialog.TYPE_ROLE, ModelerDialog.GROUP_ITEM) |
| 794 | + |
| 795 | + def setInactive(self): |
| 796 | + self.setForeground(0, Qt.darkGray) |
| 797 | + |
| 798 | + |
| 799 | +class TreeActionItem(QTreeWidgetItem): |
| 800 | + |
| 801 | + def __init__(self, action): |
| 802 | + QTreeWidgetItem.__init__(self) |
| 803 | + self.action = action |
| 804 | + self.setText(0, action.i18n_name) |
| 805 | + self.setIcon(0, action.getIcon()) |
| 806 | + self.setData(0, ModelerDialog.NAME_ROLE, action.name) |
| 807 | + |
| 808 | + |
| 809 | +class TreeProviderItem(QTreeWidgetItem): |
| 810 | + |
| 811 | + def __init__(self, provider, tree, toolbox): |
| 812 | + QTreeWidgetItem.__init__(self, None) |
| 813 | + self.tree = tree |
| 814 | + self.toolbox = toolbox |
| 815 | + self.provider = provider |
| 816 | + self.setIcon(0, self.provider.icon()) |
| 817 | + self.setData(0, ModelerDialog.TYPE_ROLE, ModelerDialog.PROVIDER_ITEM) |
| 818 | + self.setToolTip(0, self.provider.longName()) |
| 819 | + self.populate() |
| 820 | + |
| 821 | + def refresh(self): |
| 822 | + self.takeChildren() |
| 823 | + self.populate() |
| 824 | + |
| 825 | + def populate(self): |
| 826 | + self.toolbox.addAlgorithmsFromProvider(self.provider, self) |
0 commit comments