Skip to content

Commit

Permalink
[TIK-144] Improving the filtering methodology with fuzzy mathching.
Browse files Browse the repository at this point in the history
  • Loading branch information
masqu3rad3 committed Dec 6, 2024
1 parent cf233b1 commit 7530dec
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 55 deletions.
8 changes: 0 additions & 8 deletions tik_manager4/ui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,6 @@ def build_bars(self):
"""Build the menu bar."""
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
# menu_bar = QtWidgets.QMenuBar(self, geometry=QtCore.QRect(0, 0, 680, 18))
self.setMenuBar(self.menu_bar)
file_menu = self.menu_bar.addMenu("File")
tools_menu = self.menu_bar.addMenu("Tools")
Expand All @@ -470,10 +469,6 @@ def build_bars(self):
# File Menu
create_project = QtWidgets.QAction("&Create New Project", self)
file_menu.addAction(create_project)
# create_project_from_shotgrid = QtWidgets.QAction(
# "&Create Project from Shotgrid ", self
# )
# file_menu.addAction(create_project_from_shotgrid)
set_project = QtWidgets.QAction("&Set Project", self)
file_menu.addAction(set_project)
file_menu.addSeparator()
Expand Down Expand Up @@ -936,9 +931,6 @@ def management_lock(self):
handler = self.management_connect(management_platform)
if not handler:
return
# # create the UI extensions
# ui_extensions = management.ui_extensions[management_platform](handler, self)
# ui_extensions.add_main_menu()
wait_popup = WaitDialog(message="Syncing Project...", parent=self)
wait_popup.display()
synced = handler.sync_project()
Expand Down
21 changes: 4 additions & 17 deletions tik_manager4/ui/mcv/category_mcv.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from tik_manager4.ui.dialog.feedback import Feedback
from tik_manager4.ui.dialog.work_dialog import NewVersionDialog
from tik_manager4.ui.widgets.common import HorizontalSeparator, TikIconButton
from tik_manager4.ui.mcv.filter import FilterModel, FilterWidget

from tik_manager4.ui import pick

Expand Down Expand Up @@ -273,7 +274,7 @@ def __init__(self, parent=None):
self.setExpandsOnDoubleClick(True)

self.model = TikCategoryModel()
self.proxy_model = QtCore.QSortFilterProxyModel()
self.proxy_model = FilterModel()
self.proxy_model.setSourceModel(self.model)
self.proxy_model.setRecursiveFilteringEnabled(True)
self.setSortingEnabled(True)
Expand Down Expand Up @@ -446,15 +447,6 @@ def set_column_sizes(self, column_sizes):
for column, size in column_sizes.items():
self.setColumnWidth(int(column), size)

def filter(self, text):
"""Filter the model.
Args:
text (str): The text to be used for filtering.
"""
self.proxy_model.setFilterRegExp(
QtCore.QRegExp(text, QtCore.Qt.CaseInsensitive, QtCore.QRegExp.RegExp)
)

def header_right_click_menu(self, position):
"""Create a right click menu for the header.
Args:
Expand Down Expand Up @@ -807,13 +799,8 @@ def __init__(self, *args, **kwargs):
self.work_tree_view = TikCategoryView()
self.addWidget(self.work_tree_view)

self.filter_le = QtWidgets.QLineEdit()
self.addWidget(self.filter_le)
self.filter_le.textChanged.connect(self.work_tree_view.filter)
self.filter_le.setPlaceholderText("Filter")
self.filter_le.setClearButtonEnabled(True)
self.filter_le.setFocus()
self.filter_le.returnPressed.connect(self.work_tree_view.setFocus)
self.filter_widget = FilterWidget(self.work_tree_view.proxy_model)
self.addWidget(self.filter_widget)

self.task = None

Expand Down
185 changes: 185 additions & 0 deletions tik_manager4/ui/mcv/filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"""Credits to minimalefforttech for the FilterModel class
Based on the original code from
https://github.com/minimalefforttech
"""

from difflib import SequenceMatcher

from tik_manager4.ui.widgets.common import TikIconButton

from tik_manager4.ui.Qt import QtWidgets, QtCore, QtGui

class FilterModel(QtCore.QSortFilterProxyModel):
"""A simple filter model based on sequencematcher quick_ratio.
ratio is 0-1 value for a match.
Note: This is not performant, it is an example, ideally you would look into precaching or difflib.get_close_matches() for many items
https://docs.python.org/3/library/difflib.html
"""

def __init__(self, ratio: float = 0.6, parent=None):
super().__init__(parent)
self._filter_text = ""
self._ratio = ratio
self._show_all = False
self.sort(0, QtCore.Qt.AscendingOrder)

@QtCore.Slot(str)
def set_filter_text(self, text: str):
self._filter_text = str(text).lower()
self.invalidate()

@QtCore.Slot(float)
def set_ratio(self, ratio: float):
self._ratio = float(ratio)
self.invalidate()

@QtCore.Slot(bool)
def set_show_all(self, show_all: bool):
self._show_all = bool(show_all)
self.invalidate()

def filterAcceptsColumn(
self, source_column: int, source_parent: QtCore.QModelIndex
) -> bool:
return True

def filterAcceptsRow(
self, source_row: int, source_parent: QtCore.QModelIndex
) -> bool:
if self._ratio <= 0.0 or self._show_all or not self._filter_text:
# Nothing set, show everything
return True

# Case-insensitive
text = (
self.sourceModel().index(source_row, 0, source_parent).data() or ""
).lower()
if not text:
return False
if self._filter_text in text:
return True
# First parameter is optional filtering of "junk" text like spaces, default is usually fine.
# ratio is more accurate but doesn't provide much in this context, real_quick_ratio is not accurate enough.
ratio = SequenceMatcher(None, self._filter_text, text).quick_ratio()
# ratio = get_close_matches(self._filter_text, [text], 1, self._ratio)
return ratio >= self._ratio

def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex) -> bool:
if not self._filter_text or self._show_all:
return left.row() < right.row()

left_ratio = (
SequenceMatcher(None, self._filter_text, left.data().lower()).quick_ratio()
if left.data()
else 0.0
)
right_ratio = (
SequenceMatcher(None, self._filter_text, right.data().lower()).quick_ratio()
if right.data()
else 0.0
)
# Sort text ascending so ratio is flipped
return left_ratio > right_ratio

def data(self, index, role=QtCore.Qt.DisplayRole):
if not self._show_all or role != QtCore.Qt.ForegroundRole or self._ratio <= 0.0:
return super().data(index, role)

# Get and normalize text for comparison
filter_text = self._filter_text or ""
index_text = (super().data(index, QtCore.Qt.DisplayRole) or "").lower()
ratio = SequenceMatcher(None, filter_text, index_text).quick_ratio()

# if ratio < self._ratio:
if ratio < self._ratio and filter_text:
# Precompute falloff
t = ratio * (1.0 / self._ratio)
luminance = (1 - t) * 20 + t * 255
scale_factor = luminance / 255

# Get the original foreground color or default to white
original_brush = super().data(index, QtCore.Qt.ForegroundRole)
if original_brush and isinstance(original_brush, QtGui.QBrush):
color = original_brush.color()
r, g, b = color.red(), color.green(), color.blue()
else:
r = g = b = 255

# Scale colors and clamp
r = max(0, min(255, int(r * scale_factor)))
g = max(0, min(255, int(g * scale_factor)))
b = max(0, min(255, int(b * scale_factor)))

return QtGui.QBrush(QtGui.QColor(r, g, b))

return super().data(index, role)


class FilterWidget(QtWidgets.QWidget):
"""Filter widget with a line edit button and a slider."""
def __init__(self, filter_model):
super().__init__()
if not isinstance(filter_model, FilterModel):
raise ValueError("Invalid model")
self._filter_model = filter_model

self.master_layout = QtWidgets.QVBoxLayout(self)
self.master_layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.master_layout)

self.basic_lay = QtWidgets.QHBoxLayout()
# self.master_layout.addLayout(self.basic_lay)

self.filter_le = QtWidgets.QLineEdit()
self.filter_le.setPlaceholderText("Filter")
self.filter_le.setClearButtonEnabled(True)
self.filter_le.setFocus()

self.basic_lay.addWidget(self.filter_le)

self.adv_button = TikIconButton(icon_name="arrow_right", size=22)
self.basic_lay.addWidget(self.adv_button)

self.adv_widget = QtWidgets.QWidget()
# no margins
self.adv_widget.setContentsMargins(0, 0, 0, 0)
# self.master_layout.addWidget(self.adv_widget)
self.adv_widget.setVisible(False)

self.adv_lay = QtWidgets.QHBoxLayout()
# no margins
self.adv_lay.setContentsMargins(0, 0, 0, 0)
self.adv_widget.setLayout(self.adv_lay)
self.show_all_cb = QtWidgets.QCheckBox("All")
self.show_all_cb.setChecked(False)
self.show_all_cb.setToolTip("Show all items")
self.adv_lay.addWidget(self.show_all_cb)

self.ratio_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.ratio_slider.setRange(1, 100)
self.ratio_slider.setSingleStep(0.1)
self.ratio_slider.setValue(60)
self.ratio_slider.setToolTip("Ratio")
self.adv_lay.addWidget(self.ratio_slider)

# add first the advanced widget
self.master_layout.addWidget(self.adv_widget)
# then the basic layout
self.master_layout.addLayout(self.basic_lay)

# SIGNALS
self.filter_le.textChanged.connect(self._filter_model.set_filter_text)
self.show_all_cb.toggled.connect(self._filter_model.set_show_all)
self.ratio_slider.valueChanged.connect(self.on_set_ratio)
self.adv_button.clicked.connect(self.toggle_advanced)

def on_set_ratio(self, value):
self._filter_model.set_ratio(value / 100.0)

def toggle_advanced(self):
self.adv_widget.setVisible(not self.adv_widget.isVisible())
self.adv_button.set_icon("arrow_up" if self.adv_widget.isVisible() else "arrow_right")




27 changes: 11 additions & 16 deletions tik_manager4/ui/mcv/subproject_mcv.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import tik_manager4.ui.dialog.subproject_dialog
import tik_manager4.ui.dialog.task_dialog
from tik_manager4.ui.widgets.common import HorizontalSeparator, TikIconButton
from tik_manager4.ui.mcv.filter import FilterModel, FilterWidget
from tik_manager4.ui.dialog.feedback import Feedback
import tik_manager4
from tik_manager4.ui import pick
Expand Down Expand Up @@ -458,11 +459,11 @@ def set_project(self, project_obj):
self.setModel(self.proxy_model)
self.model.populate()

def filter(self, text):
# pass
self.proxy_model.setFilterRegExp(
QtCore.QRegExp(text, QtCore.Qt.CaseInsensitive, QtCore.QRegExp.RegExp)
)
# def filter(self, text):
# # pass
# self.proxy_model.setFilterRegExp(
# QtCore.QRegExp(text, QtCore.Qt.CaseInsensitive, QtCore.QRegExp.RegExp)
# )

def header_right_click_menu(self, position):
"""Creates a right click menu for the header"""
Expand Down Expand Up @@ -658,11 +659,9 @@ def delete_sub_project(self, item):
else:
return


class ProxyModel(QtCore.QSortFilterProxyModel):
class ProxyModel(FilterModel):
def __init__(self, parent=None):
super(ProxyModel, self).__init__(parent)
pass
super(ProxyModel, self).__init__(parent=parent)

def filterAcceptsRow(self, source_row, source_parent):
model = self.sourceModel()
Expand Down Expand Up @@ -703,19 +702,15 @@ def __init__(self, project_obj, recursive_enabled=True, right_click_enabled=True

self.sub_view = TikSubView(project_obj, right_click_enabled=right_click_enabled)
self.addWidget(self.sub_view)
self.filter_le = QtWidgets.QLineEdit()
self.addWidget(self.filter_le)
self.filter_le.textChanged.connect(self.sub_view.filter)
self.filter_le.setPlaceholderText("Filter")
self.filter_le.setClearButtonEnabled(True)
self.filter_le.setFocus()

self.filter_widget = FilterWidget(self.sub_view.proxy_model)
self.addWidget(self.filter_widget)

if recursive_enabled:
self.sub_view.set_recursive_task_scan(self.recursive_search_cb.isChecked())
self.recursive_search_cb.stateChanged.connect(
self.sub_view.set_recursive_task_scan
)
self.filter_le.returnPressed.connect(self.sub_view.setFocus)

# Hide all columns except the first one
for idx in range(1, self.sub_view.header().count()):
Expand Down
19 changes: 5 additions & 14 deletions tik_manager4/ui/mcv/task_mcv.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from tik_manager4.ui.dialog.feedback import Feedback
import tik_manager4.ui.dialog.task_dialog
from tik_manager4.ui.widgets.common import HorizontalSeparator, TikIconButton
from tik_manager4.ui.mcv.filter import FilterModel, FilterWidget

from tik_manager4.ui import pick

Expand Down Expand Up @@ -127,7 +128,7 @@ def __init__(self):
self.setRootIsDecorated(False)

self.model = TikTaskModel()
self.proxy_model = QtCore.QSortFilterProxyModel()
self.proxy_model = FilterModel(parent=self)
self.proxy_model.setSourceModel(self.model)
self.proxy_model.setRecursiveFilteringEnabled(True)
self.setSortingEnabled(True)
Expand Down Expand Up @@ -266,12 +267,6 @@ def add_tasks(self, tasks):
_ = [self.model.append_task(x) for x in tasks]
self.expandAll()

def filter(self, text):
"""Filter the model"""
self.proxy_model.setFilterRegExp(
QtCore.QRegExp(text, QtCore.Qt.CaseInsensitive, QtCore.QRegExp.RegExp)
)

def header_right_click_menu(self, position):
menu = QtWidgets.QMenu(self)

Expand Down Expand Up @@ -422,13 +417,9 @@ def __init__(self):

self.task_view = TikTaskView()
self.addWidget(self.task_view)
self.filter_le = QtWidgets.QLineEdit()
self.addWidget(self.filter_le)
self.filter_le.textChanged.connect(self.task_view.filter)
self.filter_le.setPlaceholderText("Filter")
self.filter_le.setClearButtonEnabled(True)
self.filter_le.setFocus()
self.filter_le.returnPressed.connect(self.task_view.setFocus)

self.filter_widget = FilterWidget(self.task_view.proxy_model)
self.addWidget(self.filter_widget)

# Hide all columns except the first one
for idx in range(1, self.task_view.header().count()):
Expand Down

0 comments on commit 7530dec

Please sign in to comment.