mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into feature/AY-2218_Plugin-hooks-Loader-and-Scene-Inventory
This commit is contained in:
commit
61153984a7
10 changed files with 612 additions and 280 deletions
|
|
@ -8,7 +8,7 @@ targeted by task types and names.
|
|||
|
||||
Placeholders are created using placeholder plugins which should care about
|
||||
logic and data of placeholder items. 'PlaceholderItem' is used to keep track
|
||||
about it's progress.
|
||||
about its progress.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -17,6 +17,7 @@ import collections
|
|||
import copy
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import ayon_api
|
||||
from ayon_api import (
|
||||
get_folders,
|
||||
get_folder_by_path,
|
||||
|
|
@ -60,6 +61,32 @@ from ayon_core.pipeline.create import (
|
|||
_NOT_SET = object()
|
||||
|
||||
|
||||
class EntityResolutionError(Exception):
|
||||
"""Exception raised when entity URI resolution fails."""
|
||||
|
||||
|
||||
def resolve_entity_uri(entity_uri: str) -> str:
|
||||
"""Resolve AYON entity URI to a filesystem path for local system."""
|
||||
response = ayon_api.post(
|
||||
"resolve",
|
||||
resolveRoots=True,
|
||||
uris=[entity_uri]
|
||||
)
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError(
|
||||
f"Unable to resolve AYON entity URI filepath for "
|
||||
f"'{entity_uri}': {response.text}"
|
||||
)
|
||||
|
||||
entities = response.data[0]["entities"]
|
||||
if len(entities) != 1:
|
||||
raise EntityResolutionError(
|
||||
f"Unable to resolve AYON entity URI '{entity_uri}' to a "
|
||||
f"single filepath. Received data: {response.data}"
|
||||
)
|
||||
return entities[0]["filePath"]
|
||||
|
||||
|
||||
class TemplateNotFound(Exception):
|
||||
"""Exception raised when template does not exist."""
|
||||
pass
|
||||
|
|
@ -823,7 +850,6 @@ class AbstractTemplateBuilder(ABC):
|
|||
"""
|
||||
|
||||
host_name = self.host_name
|
||||
project_name = self.project_name
|
||||
task_name = self.current_task_name
|
||||
task_type = self.current_task_type
|
||||
|
||||
|
|
@ -835,7 +861,6 @@ class AbstractTemplateBuilder(ABC):
|
|||
"task_names": task_name
|
||||
}
|
||||
)
|
||||
|
||||
if not profile:
|
||||
raise TemplateProfileNotFound((
|
||||
"No matching profile found for task '{}' of type '{}' "
|
||||
|
|
@ -843,6 +868,22 @@ class AbstractTemplateBuilder(ABC):
|
|||
).format(task_name, task_type, host_name))
|
||||
|
||||
path = profile["path"]
|
||||
if not path:
|
||||
raise TemplateLoadFailed((
|
||||
"Template path is not set.\n"
|
||||
"Path need to be set in {}\\Template Workfile Build "
|
||||
"Settings\\Profiles"
|
||||
).format(host_name.title()))
|
||||
|
||||
resolved_path = self.resolve_template_path(path)
|
||||
if not resolved_path or not os.path.exists(resolved_path):
|
||||
raise TemplateNotFound(
|
||||
"Template file found in AYON settings for task '{}' with host "
|
||||
"'{}' does not exists. (Not found : {})".format(
|
||||
task_name, host_name, resolved_path)
|
||||
)
|
||||
|
||||
self.log.info(f"Found template at: '{resolved_path}'")
|
||||
|
||||
# switch to remove placeholders after they are used
|
||||
keep_placeholder = profile.get("keep_placeholder")
|
||||
|
|
@ -852,44 +893,86 @@ class AbstractTemplateBuilder(ABC):
|
|||
if keep_placeholder is None:
|
||||
keep_placeholder = True
|
||||
|
||||
if not path:
|
||||
raise TemplateLoadFailed((
|
||||
"Template path is not set.\n"
|
||||
"Path need to be set in {}\\Template Workfile Build "
|
||||
"Settings\\Profiles"
|
||||
).format(host_name.title()))
|
||||
|
||||
# Try to fill path with environments and anatomy roots
|
||||
anatomy = Anatomy(project_name)
|
||||
fill_data = {
|
||||
key: value
|
||||
for key, value in os.environ.items()
|
||||
return {
|
||||
"path": resolved_path,
|
||||
"keep_placeholder": keep_placeholder,
|
||||
"create_first_version": create_first_version
|
||||
}
|
||||
|
||||
fill_data["root"] = anatomy.roots
|
||||
fill_data["project"] = {
|
||||
"name": project_name,
|
||||
"code": anatomy.project_code,
|
||||
}
|
||||
def resolve_template_path(self, path, fill_data=None) -> str:
|
||||
"""Resolve the template path.
|
||||
|
||||
path = self.resolve_template_path(path, fill_data)
|
||||
By default, this:
|
||||
- Resolves AYON entity URI to a filesystem path
|
||||
- Returns path directly if it exists on disk.
|
||||
- Resolves template keys through anatomy and environment variables.
|
||||
|
||||
This can be overridden in host integrations to perform additional
|
||||
resolving over the template. Like, `hou.text.expandString` in Houdini.
|
||||
It's recommended to still call the super().resolve_template_path()
|
||||
to ensure the basic resolving is done across all integrations.
|
||||
|
||||
Arguments:
|
||||
path (str): The input path.
|
||||
fill_data (dict[str, str]): Deprecated. This is computed inside
|
||||
the method using the current environment and project settings.
|
||||
Used to be the data to use for template formatting.
|
||||
|
||||
Returns:
|
||||
str: The resolved path.
|
||||
|
||||
"""
|
||||
|
||||
# If the path is an AYON entity URI, then resolve the filepath
|
||||
# through the backend
|
||||
if path.startswith("ayon+entity://") or path.startswith("ayon://"):
|
||||
# This is a special case where the path is an AYON entity URI
|
||||
# We need to resolve it to a filesystem path
|
||||
resolved_path = resolve_entity_uri(path)
|
||||
return resolved_path
|
||||
|
||||
# If the path is set and it's found on disk, return it directly
|
||||
if path and os.path.exists(path):
|
||||
self.log.info("Found template at: '{}'".format(path))
|
||||
return {
|
||||
"path": path,
|
||||
"keep_placeholder": keep_placeholder,
|
||||
"create_first_version": create_first_version
|
||||
return path
|
||||
|
||||
# We may have path for another platform, like C:/path/to/file
|
||||
# or a path with template keys, like {project[code]} or both.
|
||||
# Try to fill path with environments and anatomy roots
|
||||
project_name = self.project_name
|
||||
anatomy = Anatomy(project_name)
|
||||
|
||||
# Simple check whether the path contains any template keys
|
||||
if "{" in path:
|
||||
fill_data = {
|
||||
key: value
|
||||
for key, value in os.environ.items()
|
||||
}
|
||||
fill_data["root"] = anatomy.roots
|
||||
fill_data["project"] = {
|
||||
"name": project_name,
|
||||
"code": anatomy.project_code,
|
||||
}
|
||||
|
||||
solved_path = None
|
||||
# Format the template using local fill data
|
||||
result = StringTemplate.format_template(path, fill_data)
|
||||
if not result.solved:
|
||||
return path
|
||||
|
||||
path = result.normalized()
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
# If the path were set in settings using a Windows path and we
|
||||
# are now on a Linux system, we try to convert the solved path to
|
||||
# the current platform.
|
||||
while True:
|
||||
try:
|
||||
solved_path = anatomy.path_remapper(path)
|
||||
except KeyError as missing_key:
|
||||
raise KeyError(
|
||||
"Could not solve key '{}' in template path '{}'".format(
|
||||
missing_key, path))
|
||||
f"Could not solve key '{missing_key}'"
|
||||
f" in template path '{path}'"
|
||||
)
|
||||
|
||||
if solved_path is None:
|
||||
solved_path = path
|
||||
|
|
@ -898,40 +981,7 @@ class AbstractTemplateBuilder(ABC):
|
|||
path = solved_path
|
||||
|
||||
solved_path = os.path.normpath(solved_path)
|
||||
if not os.path.exists(solved_path):
|
||||
raise TemplateNotFound(
|
||||
"Template found in AYON settings for task '{}' with host "
|
||||
"'{}' does not exists. (Not found : {})".format(
|
||||
task_name, host_name, solved_path))
|
||||
|
||||
self.log.info("Found template at: '{}'".format(solved_path))
|
||||
|
||||
return {
|
||||
"path": solved_path,
|
||||
"keep_placeholder": keep_placeholder,
|
||||
"create_first_version": create_first_version
|
||||
}
|
||||
|
||||
def resolve_template_path(self, path, fill_data) -> str:
|
||||
"""Resolve the template path.
|
||||
|
||||
By default, this does nothing except returning the path directly.
|
||||
|
||||
This can be overridden in host integrations to perform additional
|
||||
resolving over the template. Like, `hou.text.expandString` in Houdini.
|
||||
|
||||
Arguments:
|
||||
path (str): The input path.
|
||||
fill_data (dict[str, str]): Data to use for template formatting.
|
||||
|
||||
Returns:
|
||||
str: The resolved path.
|
||||
|
||||
"""
|
||||
result = StringTemplate.format_template(path, fill_data)
|
||||
if result.solved:
|
||||
path = result.normalized()
|
||||
return path
|
||||
return solved_path
|
||||
|
||||
def emit_event(self, topic, data=None, source=None) -> Event:
|
||||
return self._event_system.emit(topic, data, source)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class ExtractOIIOTranscode(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
# Supported extensions
|
||||
supported_exts = ["exr", "jpg", "jpeg", "png", "dpx"]
|
||||
supported_exts = {"exr", "jpg", "jpeg", "png", "dpx"}
|
||||
|
||||
# Configurable by Settings
|
||||
profiles = None
|
||||
|
|
|
|||
|
|
@ -135,11 +135,11 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
]
|
||||
|
||||
# Supported extensions
|
||||
image_exts = ["exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif"]
|
||||
video_exts = ["mov", "mp4"]
|
||||
supported_exts = image_exts + video_exts
|
||||
image_exts = {"exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif"}
|
||||
video_exts = {"mov", "mp4"}
|
||||
supported_exts = image_exts | video_exts
|
||||
|
||||
alpha_exts = ["exr", "png", "dpx"]
|
||||
alpha_exts = {"exr", "png", "dpx"}
|
||||
|
||||
# Preset attributes
|
||||
profiles = []
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from __future__ import annotations
|
||||
import contextlib
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
import ayon_api
|
||||
|
||||
|
|
@ -140,6 +142,7 @@ class TaskTypeItem:
|
|||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectItem:
|
||||
"""Item representing folder entity on a server.
|
||||
|
||||
|
|
@ -150,21 +153,14 @@ class ProjectItem:
|
|||
active (Union[str, None]): Parent folder id. If 'None' then project
|
||||
is parent.
|
||||
"""
|
||||
|
||||
def __init__(self, name, active, is_library, icon=None):
|
||||
self.name = name
|
||||
self.active = active
|
||||
self.is_library = is_library
|
||||
if icon is None:
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.book" if is_library else "fa.map",
|
||||
"color": get_default_entity_icon_color(),
|
||||
}
|
||||
self.icon = icon
|
||||
name: str
|
||||
active: bool
|
||||
is_library: bool
|
||||
icon: dict[str, Any]
|
||||
is_pinned: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_entity(cls, project_entity):
|
||||
def from_entity(cls, project_entity: dict[str, Any]) -> "ProjectItem":
|
||||
"""Creates folder item from entity.
|
||||
|
||||
Args:
|
||||
|
|
@ -174,10 +170,16 @@ class ProjectItem:
|
|||
ProjectItem: Project item.
|
||||
|
||||
"""
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.book" if project_entity["library"] else "fa.map",
|
||||
"color": get_default_entity_icon_color(),
|
||||
}
|
||||
return cls(
|
||||
project_entity["name"],
|
||||
project_entity["active"],
|
||||
project_entity["library"],
|
||||
icon
|
||||
)
|
||||
|
||||
def to_data(self):
|
||||
|
|
@ -208,16 +210,18 @@ class ProjectItem:
|
|||
return cls(**data)
|
||||
|
||||
|
||||
def _get_project_items_from_entitiy(projects):
|
||||
def _get_project_items_from_entitiy(
|
||||
projects: list[dict[str, Any]]
|
||||
) -> list[ProjectItem]:
|
||||
"""
|
||||
|
||||
Args:
|
||||
projects (list[dict[str, Any]]): List of projects.
|
||||
|
||||
Returns:
|
||||
ProjectItem: Project item.
|
||||
"""
|
||||
list[ProjectItem]: Project item.
|
||||
|
||||
"""
|
||||
return [
|
||||
ProjectItem.from_entity(project)
|
||||
for project in projects
|
||||
|
|
@ -428,9 +432,20 @@ class ProjectsModel(object):
|
|||
self._projects_cache.update_data(project_items)
|
||||
return self._projects_cache.get_data()
|
||||
|
||||
def _query_projects(self):
|
||||
def _query_projects(self) -> list[ProjectItem]:
|
||||
projects = ayon_api.get_projects(fields=["name", "active", "library"])
|
||||
return _get_project_items_from_entitiy(projects)
|
||||
user = ayon_api.get_user()
|
||||
pinned_projects = (
|
||||
user
|
||||
.get("data", {})
|
||||
.get("frontendPreferences", {})
|
||||
.get("pinnedProjects")
|
||||
) or []
|
||||
pinned_projects = set(pinned_projects)
|
||||
project_items = _get_project_items_from_entitiy(list(projects))
|
||||
for project in project_items:
|
||||
project.is_pinned = project.name in pinned_projects
|
||||
return project_items
|
||||
|
||||
def _status_items_getter(self, project_entity):
|
||||
if not project_entity:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from ayon_core.lib import Logger, get_ayon_username
|
||||
from ayon_core.lib.events import QueuedEventSystem
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.settings import get_project_settings, get_studio_settings
|
||||
from ayon_core.tools.common_models import ProjectsModel, HierarchyModel
|
||||
|
||||
from .abstract import AbstractLauncherFrontEnd, AbstractLauncherBackend
|
||||
|
|
@ -85,7 +85,10 @@ class BaseLauncherController(
|
|||
def get_project_settings(self, project_name):
|
||||
if project_name in self._project_settings:
|
||||
return self._project_settings[project_name]
|
||||
settings = get_project_settings(project_name)
|
||||
if project_name:
|
||||
settings = get_project_settings(project_name)
|
||||
else:
|
||||
settings = get_studio_settings()
|
||||
self._project_settings[project_name] = settings
|
||||
return settings
|
||||
|
||||
|
|
|
|||
|
|
@ -1,154 +0,0 @@
|
|||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from ayon_core.tools.flickcharm import FlickCharm
|
||||
from ayon_core.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
RefreshButton,
|
||||
ProjectsQtModel,
|
||||
ProjectSortFilterProxy,
|
||||
)
|
||||
from ayon_core.tools.common_models import PROJECTS_MODEL_SENDER
|
||||
|
||||
|
||||
class ProjectIconView(QtWidgets.QListView):
|
||||
"""Styled ListView that allows to toggle between icon and list mode.
|
||||
|
||||
Toggling between the two modes is done by Right Mouse Click.
|
||||
"""
|
||||
|
||||
IconMode = 0
|
||||
ListMode = 1
|
||||
|
||||
def __init__(self, parent=None, mode=ListMode):
|
||||
super(ProjectIconView, self).__init__(parent=parent)
|
||||
|
||||
# Workaround for scrolling being super slow or fast when
|
||||
# toggling between the two visual modes
|
||||
self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
|
||||
self.setObjectName("IconView")
|
||||
|
||||
self._mode = None
|
||||
self.set_mode(mode)
|
||||
|
||||
def set_mode(self, mode):
|
||||
if mode == self._mode:
|
||||
return
|
||||
|
||||
self._mode = mode
|
||||
|
||||
if mode == self.IconMode:
|
||||
self.setViewMode(QtWidgets.QListView.IconMode)
|
||||
self.setResizeMode(QtWidgets.QListView.Adjust)
|
||||
self.setWrapping(True)
|
||||
self.setWordWrap(True)
|
||||
self.setGridSize(QtCore.QSize(151, 90))
|
||||
self.setIconSize(QtCore.QSize(50, 50))
|
||||
self.setSpacing(0)
|
||||
self.setAlternatingRowColors(False)
|
||||
|
||||
self.setProperty("mode", "icon")
|
||||
self.style().polish(self)
|
||||
|
||||
self.verticalScrollBar().setSingleStep(30)
|
||||
|
||||
elif self.ListMode:
|
||||
self.setProperty("mode", "list")
|
||||
self.style().polish(self)
|
||||
|
||||
self.setViewMode(QtWidgets.QListView.ListMode)
|
||||
self.setResizeMode(QtWidgets.QListView.Adjust)
|
||||
self.setWrapping(False)
|
||||
self.setWordWrap(False)
|
||||
self.setIconSize(QtCore.QSize(20, 20))
|
||||
self.setGridSize(QtCore.QSize(100, 25))
|
||||
self.setSpacing(0)
|
||||
self.setAlternatingRowColors(False)
|
||||
|
||||
self.verticalScrollBar().setSingleStep(34)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == QtCore.Qt.RightButton:
|
||||
self.set_mode(int(not self._mode))
|
||||
return super(ProjectIconView, self).mousePressEvent(event)
|
||||
|
||||
|
||||
class ProjectsWidget(QtWidgets.QWidget):
|
||||
"""Projects Page"""
|
||||
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent=None):
|
||||
super(ProjectsWidget, self).__init__(parent=parent)
|
||||
|
||||
header_widget = QtWidgets.QWidget(self)
|
||||
|
||||
projects_filter_text = PlaceholderLineEdit(header_widget)
|
||||
projects_filter_text.setPlaceholderText("Filter projects...")
|
||||
|
||||
refresh_btn = RefreshButton(header_widget)
|
||||
|
||||
header_layout = QtWidgets.QHBoxLayout(header_widget)
|
||||
header_layout.setContentsMargins(0, 0, 0, 0)
|
||||
header_layout.addWidget(projects_filter_text, 1)
|
||||
header_layout.addWidget(refresh_btn, 0)
|
||||
|
||||
projects_view = ProjectIconView(parent=self)
|
||||
projects_view.setSelectionMode(QtWidgets.QListView.NoSelection)
|
||||
flick = FlickCharm(parent=self)
|
||||
flick.activateOn(projects_view)
|
||||
projects_model = ProjectsQtModel(controller)
|
||||
projects_proxy_model = ProjectSortFilterProxy()
|
||||
projects_proxy_model.setSourceModel(projects_model)
|
||||
|
||||
projects_view.setModel(projects_proxy_model)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(header_widget, 0)
|
||||
main_layout.addWidget(projects_view, 1)
|
||||
|
||||
projects_view.clicked.connect(self._on_view_clicked)
|
||||
projects_model.refreshed.connect(self.refreshed)
|
||||
projects_filter_text.textChanged.connect(
|
||||
self._on_project_filter_change)
|
||||
refresh_btn.clicked.connect(self._on_refresh_clicked)
|
||||
|
||||
controller.register_event_callback(
|
||||
"projects.refresh.finished",
|
||||
self._on_projects_refresh_finished
|
||||
)
|
||||
|
||||
self._controller = controller
|
||||
|
||||
self._projects_view = projects_view
|
||||
self._projects_model = projects_model
|
||||
self._projects_proxy_model = projects_proxy_model
|
||||
|
||||
def has_content(self):
|
||||
"""Model has at least one project.
|
||||
|
||||
Returns:
|
||||
bool: True if there is any content in the model.
|
||||
"""
|
||||
|
||||
return self._projects_model.has_content()
|
||||
|
||||
def _on_view_clicked(self, index):
|
||||
if not index.isValid():
|
||||
return
|
||||
model = index.model()
|
||||
flags = model.flags(index)
|
||||
if not flags & QtCore.Qt.ItemIsEnabled:
|
||||
return
|
||||
project_name = index.data(QtCore.Qt.DisplayRole)
|
||||
self._controller.set_selected_project(project_name)
|
||||
|
||||
def _on_project_filter_change(self, text):
|
||||
self._projects_proxy_model.setFilterFixedString(text)
|
||||
|
||||
def _on_refresh_clicked(self):
|
||||
self._controller.refresh()
|
||||
|
||||
def _on_projects_refresh_finished(self, event):
|
||||
if event["sender"] != PROJECTS_MODEL_SENDER:
|
||||
self._projects_model.refresh()
|
||||
|
|
@ -3,9 +3,13 @@ from qtpy import QtWidgets, QtCore, QtGui
|
|||
from ayon_core import style, resources
|
||||
|
||||
from ayon_core.tools.launcher.control import BaseLauncherController
|
||||
from ayon_core.tools.utils import MessageOverlayObject
|
||||
from ayon_core.tools.utils import (
|
||||
MessageOverlayObject,
|
||||
PlaceholderLineEdit,
|
||||
RefreshButton,
|
||||
ProjectsWidget,
|
||||
)
|
||||
|
||||
from .projects_widget import ProjectsWidget
|
||||
from .hierarchy_page import HierarchyPage
|
||||
from .actions_widget import ActionsWidget
|
||||
|
||||
|
|
@ -50,7 +54,25 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
pages_widget = QtWidgets.QWidget(content_body)
|
||||
|
||||
# - First page - Projects
|
||||
projects_page = ProjectsWidget(controller, pages_widget)
|
||||
projects_page = QtWidgets.QWidget(pages_widget)
|
||||
projects_header_widget = QtWidgets.QWidget(projects_page)
|
||||
|
||||
projects_filter_text = PlaceholderLineEdit(projects_header_widget)
|
||||
projects_filter_text.setPlaceholderText("Filter projects...")
|
||||
|
||||
refresh_btn = RefreshButton(projects_header_widget)
|
||||
|
||||
projects_header_layout = QtWidgets.QHBoxLayout(projects_header_widget)
|
||||
projects_header_layout.setContentsMargins(0, 0, 0, 0)
|
||||
projects_header_layout.addWidget(projects_filter_text, 1)
|
||||
projects_header_layout.addWidget(refresh_btn, 0)
|
||||
|
||||
projects_widget = ProjectsWidget(controller, pages_widget)
|
||||
|
||||
projects_layout = QtWidgets.QVBoxLayout(projects_page)
|
||||
projects_layout.setContentsMargins(0, 0, 0, 0)
|
||||
projects_layout.addWidget(projects_header_widget, 0)
|
||||
projects_layout.addWidget(projects_widget, 1)
|
||||
|
||||
# - Second page - Hierarchy (folders & tasks)
|
||||
hierarchy_page = HierarchyPage(controller, pages_widget)
|
||||
|
|
@ -102,12 +124,16 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
page_slide_anim.setEndValue(1.0)
|
||||
page_slide_anim.setEasingCurve(QtCore.QEasingCurve.OutQuad)
|
||||
|
||||
projects_page.refreshed.connect(self._on_projects_refresh)
|
||||
refresh_btn.clicked.connect(self._on_refresh_request)
|
||||
projects_widget.refreshed.connect(self._on_projects_refresh)
|
||||
|
||||
actions_refresh_timer.timeout.connect(
|
||||
self._on_actions_refresh_timeout)
|
||||
page_slide_anim.valueChanged.connect(
|
||||
self._on_page_slide_value_changed)
|
||||
page_slide_anim.finished.connect(self._on_page_slide_finished)
|
||||
projects_filter_text.textChanged.connect(
|
||||
self._on_project_filter_change)
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.project.changed",
|
||||
|
|
@ -142,6 +168,7 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
self._pages_widget = pages_widget
|
||||
self._pages_layout = pages_layout
|
||||
self._projects_page = projects_page
|
||||
self._projects_widget = projects_widget
|
||||
self._hierarchy_page = hierarchy_page
|
||||
self._actions_widget = actions_widget
|
||||
# self._action_history = action_history
|
||||
|
|
@ -194,6 +221,12 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
elif self._is_on_projects_page:
|
||||
self._go_to_hierarchy_page(project_name)
|
||||
|
||||
def _on_project_filter_change(self, text):
|
||||
self._projects_widget.set_name_filter(text)
|
||||
|
||||
def _on_refresh_request(self):
|
||||
self._controller.refresh()
|
||||
|
||||
def _on_projects_refresh(self):
|
||||
# Refresh only actions on projects page
|
||||
if self._is_on_projects_page:
|
||||
|
|
@ -201,7 +234,7 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
# No projects were found -> go back to projects page
|
||||
if not self._projects_page.has_content():
|
||||
if not self._projects_widget.has_content():
|
||||
self._go_to_projects_page()
|
||||
return
|
||||
|
||||
|
|
@ -280,6 +313,9 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
def _go_to_projects_page(self):
|
||||
if self._is_on_projects_page:
|
||||
return
|
||||
|
||||
# Deselect project in projects widget
|
||||
self._projects_widget.set_selected_project(None)
|
||||
self._is_on_projects_page = True
|
||||
self._hierarchy_page.set_page_visible(False)
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from .widgets import (
|
|||
from .views import (
|
||||
DeselectableTreeView,
|
||||
TreeView,
|
||||
ListView,
|
||||
)
|
||||
from .error_dialog import ErrorMessageBox
|
||||
from .lib import (
|
||||
|
|
@ -61,6 +62,7 @@ from .dialogs import (
|
|||
)
|
||||
from .projects_widget import (
|
||||
ProjectsCombobox,
|
||||
ProjectsWidget,
|
||||
ProjectsQtModel,
|
||||
ProjectSortFilterProxy,
|
||||
PROJECT_NAME_ROLE,
|
||||
|
|
@ -114,6 +116,7 @@ __all__ = (
|
|||
|
||||
"DeselectableTreeView",
|
||||
"TreeView",
|
||||
"ListView",
|
||||
|
||||
"ErrorMessageBox",
|
||||
|
||||
|
|
@ -145,6 +148,7 @@ __all__ = (
|
|||
"PopupUpdateKeys",
|
||||
|
||||
"ProjectsCombobox",
|
||||
"ProjectsWidget",
|
||||
"ProjectsQtModel",
|
||||
"ProjectSortFilterProxy",
|
||||
"PROJECT_NAME_ROLE",
|
||||
|
|
|
|||
|
|
@ -1,21 +1,69 @@
|
|||
from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
import typing
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from ayon_core.tools.common_models import PROJECTS_MODEL_SENDER
|
||||
from ayon_core.tools.common_models import (
|
||||
ProjectItem,
|
||||
PROJECTS_MODEL_SENDER,
|
||||
)
|
||||
|
||||
from .views import ListView
|
||||
from .lib import RefreshThread, get_qt_icon
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import TypedDict
|
||||
|
||||
class ExpectedProjectSelectionData(TypedDict):
|
||||
name: Optional[str]
|
||||
current: Optional[str]
|
||||
selected: Optional[str]
|
||||
|
||||
class ExpectedSelectionData(TypedDict):
|
||||
project: ExpectedProjectSelectionData
|
||||
|
||||
|
||||
PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1
|
||||
PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 2
|
||||
PROJECT_IS_LIBRARY_ROLE = QtCore.Qt.UserRole + 3
|
||||
PROJECT_IS_CURRENT_ROLE = QtCore.Qt.UserRole + 4
|
||||
LIBRARY_PROJECT_SEPARATOR_ROLE = QtCore.Qt.UserRole + 5
|
||||
PROJECT_IS_PINNED_ROLE = QtCore.Qt.UserRole + 5
|
||||
LIBRARY_PROJECT_SEPARATOR_ROLE = QtCore.Qt.UserRole + 6
|
||||
|
||||
|
||||
class AbstractProjectController(ABC):
|
||||
@abstractmethod
|
||||
def register_event_callback(self, topic: str, callback: Callable):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_project_items(
|
||||
self, sender: Optional[str] = None
|
||||
) -> list[str]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_selected_project(self, project_name: str):
|
||||
pass
|
||||
|
||||
# These are required only if widget should handle expected selection
|
||||
@abstractmethod
|
||||
def expected_project_selected(self, project_name: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_expected_selection_data(self) -> "ExpectedSelectionData":
|
||||
pass
|
||||
|
||||
|
||||
class ProjectsQtModel(QtGui.QStandardItemModel):
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller):
|
||||
super(ProjectsQtModel, self).__init__()
|
||||
def __init__(self, controller: AbstractProjectController):
|
||||
super().__init__()
|
||||
self._controller = controller
|
||||
|
||||
self._project_items = {}
|
||||
|
|
@ -213,7 +261,7 @@ class ProjectsQtModel(QtGui.QStandardItemModel):
|
|||
else:
|
||||
self.refreshed.emit()
|
||||
|
||||
def _fill_items(self, project_items):
|
||||
def _fill_items(self, project_items: list[ProjectItem]):
|
||||
new_project_names = {
|
||||
project_item.name
|
||||
for project_item in project_items
|
||||
|
|
@ -252,6 +300,7 @@ class ProjectsQtModel(QtGui.QStandardItemModel):
|
|||
item.setData(project_name, PROJECT_NAME_ROLE)
|
||||
item.setData(project_item.active, PROJECT_IS_ACTIVE_ROLE)
|
||||
item.setData(project_item.is_library, PROJECT_IS_LIBRARY_ROLE)
|
||||
item.setData(project_item.is_pinned, PROJECT_IS_PINNED_ROLE)
|
||||
is_current = project_name == self._current_context_project
|
||||
item.setData(is_current, PROJECT_IS_CURRENT_ROLE)
|
||||
self._project_items[project_name] = item
|
||||
|
|
@ -279,7 +328,7 @@ class ProjectsQtModel(QtGui.QStandardItemModel):
|
|||
|
||||
class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProjectSortFilterProxy, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self._filter_inactive = True
|
||||
self._filter_standard = False
|
||||
self._filter_library = False
|
||||
|
|
@ -323,26 +372,51 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
|||
return False
|
||||
|
||||
# Library separator should be before library projects
|
||||
result = self._type_sort(left_index, right_index)
|
||||
if result is not None:
|
||||
return result
|
||||
l_is_library = left_index.data(PROJECT_IS_LIBRARY_ROLE)
|
||||
r_is_library = right_index.data(PROJECT_IS_LIBRARY_ROLE)
|
||||
l_is_sep = left_index.data(LIBRARY_PROJECT_SEPARATOR_ROLE)
|
||||
r_is_sep = right_index.data(LIBRARY_PROJECT_SEPARATOR_ROLE)
|
||||
if l_is_sep:
|
||||
return bool(r_is_library)
|
||||
|
||||
if left_index.data(PROJECT_NAME_ROLE) is None:
|
||||
if r_is_sep:
|
||||
return not l_is_library
|
||||
|
||||
# Non project items should be on top
|
||||
l_project_name = left_index.data(PROJECT_NAME_ROLE)
|
||||
r_project_name = right_index.data(PROJECT_NAME_ROLE)
|
||||
if l_project_name is None:
|
||||
return True
|
||||
|
||||
if right_index.data(PROJECT_NAME_ROLE) is None:
|
||||
if r_project_name is None:
|
||||
return False
|
||||
|
||||
left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE)
|
||||
right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE)
|
||||
if right_is_active == left_is_active:
|
||||
return super(ProjectSortFilterProxy, self).lessThan(
|
||||
left_index, right_index
|
||||
)
|
||||
if right_is_active != left_is_active:
|
||||
return left_is_active
|
||||
|
||||
if left_is_active:
|
||||
l_is_pinned = left_index.data(PROJECT_IS_PINNED_ROLE)
|
||||
r_is_pinned = right_index.data(PROJECT_IS_PINNED_ROLE)
|
||||
if l_is_pinned is True and not r_is_pinned:
|
||||
return True
|
||||
return False
|
||||
|
||||
if r_is_pinned is True and not l_is_pinned:
|
||||
return False
|
||||
|
||||
# Move inactive projects to the end
|
||||
left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE)
|
||||
right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE)
|
||||
if right_is_active != left_is_active:
|
||||
return left_is_active
|
||||
|
||||
# Move library projects after standard projects
|
||||
if (
|
||||
l_is_library is not None
|
||||
and r_is_library is not None
|
||||
and l_is_library != r_is_library
|
||||
):
|
||||
return r_is_library
|
||||
return super().lessThan(left_index, right_index)
|
||||
|
||||
def filterAcceptsRow(self, source_row, source_parent):
|
||||
index = self.sourceModel().index(source_row, 0, source_parent)
|
||||
|
|
@ -415,15 +489,153 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
|||
self.invalidate()
|
||||
|
||||
|
||||
class ProjectsDelegate(QtWidgets.QStyledItemDelegate):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._pin_icon = None
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
is_pinned = index.data(PROJECT_IS_PINNED_ROLE)
|
||||
if not is_pinned:
|
||||
super().paint(painter, option, index)
|
||||
return
|
||||
opt = QtWidgets.QStyleOptionViewItem(option)
|
||||
self.initStyleOption(opt, index)
|
||||
widget = option.widget
|
||||
if widget is None:
|
||||
style = QtWidgets.QApplication.style()
|
||||
else:
|
||||
style = widget.style()
|
||||
# CE_ItemViewItem
|
||||
proxy = style.proxy()
|
||||
painter.save()
|
||||
painter.setClipRect(option.rect)
|
||||
decor_rect = proxy.subElementRect(
|
||||
QtWidgets.QStyle.SE_ItemViewItemDecoration, opt, widget
|
||||
)
|
||||
text_rect = proxy.subElementRect(
|
||||
QtWidgets.QStyle.SE_ItemViewItemText, opt, widget
|
||||
)
|
||||
proxy.drawPrimitive(
|
||||
QtWidgets.QStyle.PE_PanelItemViewItem, opt, painter, widget
|
||||
)
|
||||
mode = QtGui.QIcon.Normal
|
||||
if not opt.state & QtWidgets.QStyle.State_Enabled:
|
||||
mode = QtGui.QIcon.Disabled
|
||||
elif opt.state & QtWidgets.QStyle.State_Selected:
|
||||
mode = QtGui.QIcon.Selected
|
||||
state = QtGui.QIcon.Off
|
||||
if opt.state & QtWidgets.QStyle.State_Open:
|
||||
state = QtGui.QIcon.On
|
||||
|
||||
# Draw project icon
|
||||
opt.icon.paint(
|
||||
painter, decor_rect, opt.decorationAlignment, mode, state
|
||||
)
|
||||
|
||||
# Draw pin icon
|
||||
if index.data(PROJECT_IS_PINNED_ROLE):
|
||||
pin_icon = self._get_pin_icon()
|
||||
pin_rect = QtCore.QRect(decor_rect)
|
||||
diff = option.rect.width() - pin_rect.width()
|
||||
pin_rect.moveLeft(diff)
|
||||
pin_icon.paint(
|
||||
painter, pin_rect, opt.decorationAlignment, mode, state
|
||||
)
|
||||
|
||||
# Draw text
|
||||
if opt.text:
|
||||
if not opt.state & QtWidgets.QStyle.State_Enabled:
|
||||
cg = QtGui.QPalette.Disabled
|
||||
elif not (opt.state & QtWidgets.QStyle.State_Active):
|
||||
cg = QtGui.QPalette.Inactive
|
||||
else:
|
||||
cg = QtGui.QPalette.Normal
|
||||
|
||||
if opt.state & QtWidgets.QStyle.State_Selected:
|
||||
painter.setPen(
|
||||
opt.palette.color(cg, QtGui.QPalette.HighlightedText)
|
||||
)
|
||||
else:
|
||||
painter.setPen(opt.palette.color(cg, QtGui.QPalette.Text))
|
||||
|
||||
if opt.state & QtWidgets.QStyle.State_Editing:
|
||||
painter.setPen(opt.palette.color(cg, QtGui.QPalette.Text))
|
||||
painter.drawRect(text_rect.adjusted(0, 0, -1, -1))
|
||||
|
||||
margin = proxy.pixelMetric(
|
||||
QtWidgets.QStyle.PM_FocusFrameHMargin, None, widget
|
||||
) + 1
|
||||
text_rect.adjust(margin, 0, -margin, 0)
|
||||
# NOTE skipping some steps e.g. word wrapping and elided
|
||||
# text (adding '...' when too long).
|
||||
painter.drawText(
|
||||
text_rect,
|
||||
opt.displayAlignment,
|
||||
opt.text
|
||||
)
|
||||
|
||||
# Draw focus rect
|
||||
if opt.state & QtWidgets.QStyle.State_HasFocus:
|
||||
focus_opt = QtWidgets.QStyleOptionFocusRect()
|
||||
focus_opt.state = option.state
|
||||
focus_opt.direction = option.direction
|
||||
focus_opt.rect = option.rect
|
||||
focus_opt.fontMetrics = option.fontMetrics
|
||||
focus_opt.palette = option.palette
|
||||
|
||||
focus_opt.rect = style.subElementRect(
|
||||
QtWidgets.QCommonStyle.SE_ItemViewItemFocusRect,
|
||||
option,
|
||||
option.widget
|
||||
)
|
||||
focus_opt.state |= (
|
||||
QtWidgets.QStyle.State_KeyboardFocusChange
|
||||
| QtWidgets.QStyle.State_Item
|
||||
)
|
||||
focus_opt.backgroundColor = option.palette.color(
|
||||
(
|
||||
QtGui.QPalette.Normal
|
||||
if option.state & QtWidgets.QStyle.State_Enabled
|
||||
else QtGui.QPalette.Disabled
|
||||
),
|
||||
(
|
||||
QtGui.QPalette.Highlight
|
||||
if option.state & QtWidgets.QStyle.State_Selected
|
||||
else QtGui.QPalette.Window
|
||||
)
|
||||
)
|
||||
style.drawPrimitive(
|
||||
QtWidgets.QCommonStyle.PE_FrameFocusRect,
|
||||
focus_opt,
|
||||
painter,
|
||||
option.widget
|
||||
)
|
||||
painter.restore()
|
||||
|
||||
def _get_pin_icon(self):
|
||||
if self._pin_icon is None:
|
||||
self._pin_icon = get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": "keep",
|
||||
})
|
||||
return self._pin_icon
|
||||
|
||||
|
||||
class ProjectsCombobox(QtWidgets.QWidget):
|
||||
refreshed = QtCore.Signal()
|
||||
selection_changed = QtCore.Signal()
|
||||
selection_changed = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, controller, parent, handle_expected_selection=False):
|
||||
super(ProjectsCombobox, self).__init__(parent)
|
||||
def __init__(
|
||||
self,
|
||||
controller: AbstractProjectController,
|
||||
parent: QtWidgets.QWidget,
|
||||
handle_expected_selection: bool = False,
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
projects_combobox = QtWidgets.QComboBox(self)
|
||||
combobox_delegate = QtWidgets.QStyledItemDelegate(projects_combobox)
|
||||
combobox_delegate = ProjectsDelegate(projects_combobox)
|
||||
projects_combobox.setItemDelegate(combobox_delegate)
|
||||
projects_model = ProjectsQtModel(controller)
|
||||
projects_proxy_model = ProjectSortFilterProxy()
|
||||
|
|
@ -468,7 +680,7 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
def refresh(self):
|
||||
self._projects_model.refresh()
|
||||
|
||||
def set_selection(self, project_name):
|
||||
def set_selection(self, project_name: str):
|
||||
"""Set selection to a given project.
|
||||
|
||||
Selection change is ignored if project is not found.
|
||||
|
|
@ -480,8 +692,8 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
bool: True if selection was changed, False otherwise. NOTE:
|
||||
Selection may not be changed if project is not found, or if
|
||||
project is already selected.
|
||||
"""
|
||||
|
||||
"""
|
||||
idx = self._projects_combobox.findData(
|
||||
project_name, PROJECT_NAME_ROLE)
|
||||
if idx < 0:
|
||||
|
|
@ -491,7 +703,7 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
return True
|
||||
return False
|
||||
|
||||
def set_listen_to_selection_change(self, listen):
|
||||
def set_listen_to_selection_change(self, listen: bool):
|
||||
"""Disable listening to changes of the selection.
|
||||
|
||||
Because combobox is triggering selection change when it's model
|
||||
|
|
@ -517,11 +729,11 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
return None
|
||||
return self._projects_combobox.itemData(idx, PROJECT_NAME_ROLE)
|
||||
|
||||
def set_current_context_project(self, project_name):
|
||||
def set_current_context_project(self, project_name: str):
|
||||
self._projects_model.set_current_context_project(project_name)
|
||||
self._projects_proxy_model.invalidateFilter()
|
||||
|
||||
def set_select_item_visible(self, visible):
|
||||
def set_select_item_visible(self, visible: bool):
|
||||
self._select_item_visible = visible
|
||||
self._projects_model.set_select_item_visible(visible)
|
||||
self._update_select_item_visiblity()
|
||||
|
|
@ -559,7 +771,7 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
idx, PROJECT_NAME_ROLE)
|
||||
self._update_select_item_visiblity(project_name=project_name)
|
||||
self._controller.set_selected_project(project_name)
|
||||
self.selection_changed.emit()
|
||||
self.selection_changed.emit(project_name or "")
|
||||
|
||||
def _on_model_refresh(self):
|
||||
self._projects_proxy_model.sort(0)
|
||||
|
|
@ -614,5 +826,119 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
|
||||
|
||||
class ProjectsWidget(QtWidgets.QWidget):
|
||||
# TODO implement
|
||||
pass
|
||||
"""Projects widget showing projects in list.
|
||||
|
||||
Warnings:
|
||||
This widget does not support expected selection handling.
|
||||
|
||||
"""
|
||||
refreshed = QtCore.Signal()
|
||||
selection_changed = QtCore.Signal(str)
|
||||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
controller: AbstractProjectController,
|
||||
parent: Optional[QtWidgets.QWidget] = None
|
||||
):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
projects_view = ListView(parent=self)
|
||||
projects_view.setResizeMode(QtWidgets.QListView.Adjust)
|
||||
projects_view.setVerticalScrollMode(
|
||||
QtWidgets.QAbstractItemView.ScrollPerPixel
|
||||
)
|
||||
projects_view.setAlternatingRowColors(False)
|
||||
projects_view.setWrapping(False)
|
||||
projects_view.setWordWrap(False)
|
||||
projects_view.setSpacing(0)
|
||||
projects_delegate = ProjectsDelegate(projects_view)
|
||||
projects_view.setItemDelegate(projects_delegate)
|
||||
projects_view.activate_flick_charm()
|
||||
projects_view.set_deselectable(True)
|
||||
|
||||
projects_model = ProjectsQtModel(controller)
|
||||
projects_proxy_model = ProjectSortFilterProxy()
|
||||
projects_proxy_model.setSourceModel(projects_model)
|
||||
projects_view.setModel(projects_proxy_model)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(projects_view, 1)
|
||||
|
||||
projects_view.selectionModel().selectionChanged.connect(
|
||||
self._on_selection_change
|
||||
)
|
||||
projects_view.double_clicked.connect(self.double_clicked)
|
||||
projects_model.refreshed.connect(self._on_model_refresh)
|
||||
|
||||
controller.register_event_callback(
|
||||
"projects.refresh.finished",
|
||||
self._on_projects_refresh_finished
|
||||
)
|
||||
|
||||
self._controller = controller
|
||||
|
||||
self._projects_view = projects_view
|
||||
self._projects_model = projects_model
|
||||
self._projects_proxy_model = projects_proxy_model
|
||||
self._projects_delegate = projects_delegate
|
||||
|
||||
def refresh(self):
|
||||
self._projects_model.refresh()
|
||||
|
||||
def has_content(self) -> bool:
|
||||
"""Model has at least one project.
|
||||
|
||||
Returns:
|
||||
bool: True if there is any content in the model.
|
||||
|
||||
"""
|
||||
return self._projects_model.has_content()
|
||||
|
||||
def set_name_filter(self, text: str):
|
||||
self._projects_proxy_model.setFilterFixedString(text)
|
||||
|
||||
def get_selected_project(self) -> Optional[str]:
|
||||
selection_model = self._projects_view.selectionModel()
|
||||
for index in selection_model.selectedIndexes():
|
||||
project_name = index.data(PROJECT_NAME_ROLE)
|
||||
if project_name:
|
||||
return project_name
|
||||
return None
|
||||
|
||||
def set_selected_project(self, project_name: Optional[str]):
|
||||
if project_name is None:
|
||||
self._projects_view.clearSelection()
|
||||
self._projects_view.setCurrentIndex(QtCore.QModelIndex())
|
||||
return
|
||||
|
||||
index = self._projects_model.get_index_by_project_name(project_name)
|
||||
if not index.isValid():
|
||||
return
|
||||
proxy_index = self._projects_proxy_model.mapFromSource(index)
|
||||
if proxy_index.isValid():
|
||||
selection_model = self._projects_view.selectionModel()
|
||||
selection_model.select(
|
||||
proxy_index,
|
||||
QtCore.QItemSelectionModel.ClearAndSelect
|
||||
)
|
||||
|
||||
def _on_model_refresh(self):
|
||||
self._projects_proxy_model.sort(0)
|
||||
self._projects_proxy_model.invalidateFilter()
|
||||
self.refreshed.emit()
|
||||
|
||||
def _on_selection_change(self, new_selection, _old_selection):
|
||||
project_name = None
|
||||
for index in new_selection.indexes():
|
||||
name = index.data(PROJECT_NAME_ROLE)
|
||||
if name:
|
||||
project_name = name
|
||||
break
|
||||
self.selection_changed.emit(project_name or "")
|
||||
self._controller.set_selected_project(project_name)
|
||||
|
||||
def _on_projects_refresh_finished(self, event):
|
||||
if event["sender"] != PROJECTS_MODEL_SENDER:
|
||||
self._projects_model.refresh()
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class TreeView(QtWidgets.QTreeView):
|
|||
double_clicked = QtCore.Signal(QtGui.QMouseEvent)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TreeView, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self._deselectable = False
|
||||
|
||||
self._flick_charm_activated = False
|
||||
|
|
@ -60,12 +60,64 @@ class TreeView(QtWidgets.QTreeView):
|
|||
self.clearSelection()
|
||||
# clear the current index
|
||||
self.setCurrentIndex(QtCore.QModelIndex())
|
||||
super(TreeView, self).mousePressEvent(event)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
self.double_clicked.emit(event)
|
||||
|
||||
return super(TreeView, self).mouseDoubleClickEvent(event)
|
||||
return super().mouseDoubleClickEvent(event)
|
||||
|
||||
def activate_flick_charm(self):
|
||||
if self._flick_charm_activated:
|
||||
return
|
||||
self._flick_charm_activated = True
|
||||
self._before_flick_scroll_mode = self.verticalScrollMode()
|
||||
self._flick_charm.activateOn(self)
|
||||
self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
|
||||
|
||||
def deactivate_flick_charm(self):
|
||||
if not self._flick_charm_activated:
|
||||
return
|
||||
self._flick_charm_activated = False
|
||||
self._flick_charm.deactivateFrom(self)
|
||||
if self._before_flick_scroll_mode is not None:
|
||||
self.setVerticalScrollMode(self._before_flick_scroll_mode)
|
||||
|
||||
|
||||
class ListView(QtWidgets.QListView):
|
||||
"""A tree view that deselects on clicking on an empty area in the view"""
|
||||
double_clicked = QtCore.Signal(QtGui.QMouseEvent)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._deselectable = False
|
||||
|
||||
self._flick_charm_activated = False
|
||||
self._flick_charm = FlickCharm(parent=self)
|
||||
self._before_flick_scroll_mode = None
|
||||
|
||||
def is_deselectable(self):
|
||||
return self._deselectable
|
||||
|
||||
def set_deselectable(self, deselectable):
|
||||
self._deselectable = deselectable
|
||||
|
||||
deselectable = property(is_deselectable, set_deselectable)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if self._deselectable:
|
||||
index = self.indexAt(event.pos())
|
||||
if not index.isValid():
|
||||
# clear the selection
|
||||
self.clearSelection()
|
||||
# clear the current index
|
||||
self.setCurrentIndex(QtCore.QModelIndex())
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
self.double_clicked.emit(event)
|
||||
|
||||
return super().mouseDoubleClickEvent(event)
|
||||
|
||||
def activate_flick_charm(self):
|
||||
if self._flick_charm_activated:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue