Large refactor of ComfyUI context menus, trying to simplify and improve integration with many of the AI tasks. Also, new icons.
@@ -156,8 +156,16 @@ class GenerationService:
|
||||
return num / den
|
||||
return None
|
||||
|
||||
def action_generate_trigger(self, checked=True):
|
||||
selected_files = self.win.selected_files()
|
||||
def _default_generation_name(self, source_file):
|
||||
default_name = "generation"
|
||||
if source_file:
|
||||
path = source_file.data.get("path", "")
|
||||
if path:
|
||||
default_name = "{}_gen".format(os.path.splitext(os.path.basename(path))[0])
|
||||
return default_name
|
||||
|
||||
def action_generate_trigger(self, checked=True, source_file=None, template_id=None, open_dialog=True):
|
||||
selected_files = [source_file] if source_file else self.win.selected_files()
|
||||
if len(selected_files) > 1:
|
||||
return
|
||||
|
||||
@@ -173,11 +181,36 @@ class GenerationService:
|
||||
|
||||
source_file = selected_files[0] if selected_files else None
|
||||
templates = available_pipelines(source_file=source_file)
|
||||
win = GenerateMediaDialog(source_file=source_file, templates=templates, parent=self.win)
|
||||
if win.exec_() != QDialog.Accepted:
|
||||
return
|
||||
available_template_ids = {str(t.get("id", "")).strip() for t in templates}
|
||||
if open_dialog:
|
||||
dialog_title = "Enhance with AI" if source_file else "Create with AI"
|
||||
win = GenerateMediaDialog(
|
||||
source_file=source_file,
|
||||
templates=templates,
|
||||
preselected_template_id=template_id,
|
||||
dialog_title=dialog_title,
|
||||
parent=self.win,
|
||||
)
|
||||
if win.exec_() != QDialog.Accepted:
|
||||
return
|
||||
payload = win.get_payload()
|
||||
else:
|
||||
selected_template_id = str(template_id or "").strip()
|
||||
if not selected_template_id:
|
||||
return
|
||||
if selected_template_id not in available_template_ids:
|
||||
QMessageBox.information(
|
||||
self.win,
|
||||
"Invalid Input",
|
||||
"The selected AI action is not available for this source type.",
|
||||
)
|
||||
return
|
||||
payload = {
|
||||
"name": self._default_generation_name(source_file),
|
||||
"template_id": selected_template_id,
|
||||
"prompt": "",
|
||||
}
|
||||
|
||||
payload = win.get_payload()
|
||||
payload_name = self._next_generation_name(payload.get("name"))
|
||||
source_file_id = source_file.id if source_file else None
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<rect x="2.5" y="3" width="11" height="8" rx="1.7" stroke="#9EC8F7" stroke-width="1.3"/>
|
||||
<path d="M5 7H11" stroke="#9EC8F7" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M5 9H9.5" stroke="#9EC8F7" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M7.2 11L6.1 13.2L8.8 11" stroke="#9EC8F7" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 478 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M3.2 9.5H5.3L7.7 11.6V4.4L5.3 6.5H3.2V9.5Z" stroke="#9EC8F7" stroke-width="1.3" stroke-linejoin="round"/>
|
||||
<path d="M9.6 6.2C10.4 7 10.4 9 9.6 9.8" stroke="#9EC8F7" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M11.6 4.8C13.1 6.3 13.1 9.7 11.6 11.2" stroke="#9EC8F7" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 435 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<rect x="2.5" y="3" width="11" height="10" rx="1.8" stroke="#9EC8F7" stroke-width="1.3"/>
|
||||
<circle cx="5.4" cy="6" r="1.1" fill="#9EC8F7"/>
|
||||
<path d="M4 11L6.6 8.5L8.8 10.2L10.1 9.2L12 11" stroke="#9EC8F7" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 381 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<rect x="2.5" y="3" width="8.8" height="10" rx="1.7" stroke="#9EC8F7" stroke-width="1.3"/>
|
||||
<path d="M7 6.4L9.3 8L7 9.6V6.4Z" fill="#9EC8F7"/>
|
||||
<path d="M11.3 6.2L13.5 5.1V10.9L11.3 9.8" stroke="#9EC8F7" stroke-width="1.3" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M8 2L9.4 5.6L13 7L9.4 8.4L8 12L6.6 8.4L3 7L6.6 5.6L8 2Z" stroke="#9EC8F7" stroke-width="1.3" stroke-linejoin="round"/>
|
||||
<circle cx="8" cy="7" r="1" fill="#9EC8F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 280 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<rect x="2.5" y="3.5" width="11" height="9" rx="1.5" stroke="#9EC8F7" stroke-width="1.3"/>
|
||||
<path d="M6.2 3.5V12.5" stroke="#9EC8F7" stroke-width="1.3"/>
|
||||
<path d="M9.8 3.5V12.5" stroke="#9EC8F7" stroke-width="1.3"/>
|
||||
<path d="M2.5 8H13.5" stroke="#9EC8F7" stroke-width="1" opacity="0.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 398 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<rect x="2.3" y="5" width="6.8" height="6.8" rx="1.7" stroke="#9EC8F7" stroke-width="1.3"/>
|
||||
<rect x="4.6" y="4" width="6.8" height="6.8" rx="1.7" stroke="#9EC8F7" stroke-width="1.3" opacity="0.82"/>
|
||||
<rect x="6.9" y="3" width="6.8" height="6.8" rx="1.7" stroke="#9EC8F7" stroke-width="1.3" opacity="0.64"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 415 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<rect x="2.5" y="2.5" width="11" height="11" rx="2" stroke="#9EC8F7" stroke-width="1.3"/>
|
||||
<path d="M5.2 10.8L10.8 5.2" stroke="#9EC8F7" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M7.9 5.2H10.8V8.1" stroke="#9EC8F7" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 402 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<rect x="3" y="3" width="10" height="10" rx="2" stroke="#9EC8F7" stroke-width="1.3"/>
|
||||
<path d="M8 5V11" stroke="#9EC8F7" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M5 8H11" stroke="#9EC8F7" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 353 B |
@@ -44,12 +44,20 @@ class GenerateMediaDialog(QDialog):
|
||||
PREVIEW_WIDTH = 180
|
||||
PREVIEW_HEIGHT = 128
|
||||
|
||||
def __init__(self, source_file=None, templates=None, parent=None):
|
||||
def __init__(
|
||||
self,
|
||||
source_file=None,
|
||||
templates=None,
|
||||
preselected_template_id=None,
|
||||
dialog_title=None,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.source_file = source_file
|
||||
self.templates = templates or []
|
||||
self.preselected_template_id = str(preselected_template_id or "").strip()
|
||||
self.setObjectName("generateDialog")
|
||||
self.setWindowTitle("Generate")
|
||||
self.setWindowTitle(str(dialog_title or "AI Tools"))
|
||||
self.setMinimumWidth(620)
|
||||
self.setMinimumHeight(460)
|
||||
|
||||
@@ -118,6 +126,10 @@ class GenerateMediaDialog(QDialog):
|
||||
self.template_combo.addItem(template.get("name", ""), template.get("id", ""))
|
||||
else:
|
||||
self.template_combo.addItem("Basic Text to Image", "txt2img-basic")
|
||||
if self.preselected_template_id:
|
||||
index = self.template_combo.findData(self.preselected_template_id)
|
||||
if index >= 0:
|
||||
self.template_combo.setCurrentIndex(index)
|
||||
setup_form.addRow("Template", self.template_combo)
|
||||
|
||||
if self.source_file:
|
||||
|
||||
@@ -1146,8 +1146,13 @@ class MainWindow(updates.UpdateWatcher, QMainWindow):
|
||||
""" Preview the selected media file """
|
||||
log.info('actionPreview_File_trigger')
|
||||
|
||||
# Loop through selected files (set 1 selected file if more than 1)
|
||||
# Prefer current file, but fall back to selected real files when a generation
|
||||
# placeholder row has focus.
|
||||
f = self.files_model.current_file()
|
||||
if not f:
|
||||
selected_files = self.files_model.selected_files()
|
||||
if selected_files:
|
||||
f = selected_files[0]
|
||||
|
||||
# Bail out if no file selected
|
||||
if not f:
|
||||
|
||||
@@ -626,16 +626,25 @@ class FilesModel(QObject, updates.UpdateInterface):
|
||||
|
||||
def current_file_id(self):
|
||||
""" Get the file ID of the current files-view item, or the first selection """
|
||||
# Prefer selected rows first, since currentIndex can become stale when
|
||||
# switching between details/list views with separate selection models.
|
||||
selected_rows = self.selection_model.selectedRows(5)
|
||||
if selected_rows:
|
||||
current = self.selection_model.currentIndex()
|
||||
if current and current.isValid():
|
||||
current_id = current.sibling(current.row(), 5).data()
|
||||
if current_id and not self._is_generation_placeholder(current_id):
|
||||
return current_id
|
||||
for row_index in selected_rows:
|
||||
file_id = row_index.data()
|
||||
if file_id and not self._is_generation_placeholder(file_id):
|
||||
return file_id
|
||||
|
||||
cur = self.selection_model.currentIndex()
|
||||
|
||||
if not cur or not cur.isValid() and self.selection_model.hasSelection():
|
||||
cur = self.selection_model.selectedIndexes()[0]
|
||||
|
||||
if cur and cur.isValid():
|
||||
file_id = cur.sibling(cur.row(), 5).data()
|
||||
if self._is_generation_placeholder(file_id):
|
||||
return None
|
||||
return file_id
|
||||
if file_id and not self._is_generation_placeholder(file_id):
|
||||
return file_id
|
||||
|
||||
def current_file(self):
|
||||
""" Get the File object for the current files-view item, or the first selection """
|
||||
@@ -664,14 +673,19 @@ class FilesModel(QObject, updates.UpdateInterface):
|
||||
try:
|
||||
# Map selected indexes from proxy_model to list_proxy_model
|
||||
list_selection = QItemSelection()
|
||||
first_list_index = QModelIndex()
|
||||
for index in self.selection_model.selectedRows(0):
|
||||
list_index = self.list_proxy_model.mapFromSource(index)
|
||||
if list_index.isValid():
|
||||
list_selection.select(list_index, list_index)
|
||||
if not first_list_index.isValid():
|
||||
first_list_index = list_index
|
||||
self.list_selection_model.select(
|
||||
list_selection,
|
||||
QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows
|
||||
)
|
||||
if first_list_index.isValid():
|
||||
self.list_selection_model.setCurrentIndex(first_list_index, QItemSelectionModel.NoUpdate)
|
||||
finally:
|
||||
self._syncing_selection = False
|
||||
|
||||
@@ -683,14 +697,19 @@ class FilesModel(QObject, updates.UpdateInterface):
|
||||
try:
|
||||
# Map selected indexes from list_proxy_model to proxy_model
|
||||
tree_selection = QItemSelection()
|
||||
first_tree_index = QModelIndex()
|
||||
for index in self.list_selection_model.selectedRows(0):
|
||||
tree_index = self.list_proxy_model.mapToSource(index)
|
||||
if tree_index.isValid():
|
||||
tree_selection.select(tree_index, tree_index)
|
||||
if not first_tree_index.isValid():
|
||||
first_tree_index = tree_index
|
||||
self.selection_model.select(
|
||||
tree_selection,
|
||||
QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows
|
||||
)
|
||||
if first_tree_index.isValid():
|
||||
self.selection_model.setCurrentIndex(first_tree_index, QItemSelectionModel.NoUpdate)
|
||||
finally:
|
||||
self._syncing_selection = False
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
@file
|
||||
@brief Shared AI Tools context-menu builder for project files and timeline.
|
||||
"""
|
||||
|
||||
import os
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.QtGui import QIcon
|
||||
|
||||
from classes.app import get_app
|
||||
from classes import info
|
||||
from .menu import StyledContextMenu
|
||||
|
||||
|
||||
def _trigger_generation(win, template_id, source_file=None, open_dialog=False):
|
||||
win.generation_service.action_generate_trigger(
|
||||
source_file=source_file,
|
||||
template_id=template_id,
|
||||
open_dialog=open_dialog,
|
||||
)
|
||||
|
||||
|
||||
def _icon(name):
|
||||
icon_path = os.path.join(info.PATH, "themes", "cosmic", "images", name)
|
||||
if os.path.exists(icon_path):
|
||||
return QIcon(icon_path)
|
||||
return QIcon()
|
||||
|
||||
|
||||
def add_ai_tools_menu(win, parent_menu, source_file=None):
|
||||
_ = get_app()._tr
|
||||
media_type = str(source_file.data.get("media_type", "")) if source_file else ""
|
||||
|
||||
if source_file:
|
||||
ai_menu = StyledContextMenu(title=_("Enhance with AI"), parent=parent_menu)
|
||||
ai_menu.setIcon(_icon("tool-generate-sparkle.svg"))
|
||||
|
||||
if media_type == "image":
|
||||
action = ai_menu.addAction(_("Increase Resolution (4x)"))
|
||||
action.setIcon(_icon("ai-action-upscale.svg"))
|
||||
action.triggered.connect(
|
||||
partial(_trigger_generation, win, "upscale-realesrgan-x4", source_file, False)
|
||||
)
|
||||
ai_menu.addSeparator()
|
||||
action = ai_menu.addAction(_("Change Image Style..."))
|
||||
action.setIcon(_icon("ai-action-restyle.svg"))
|
||||
action.triggered.connect(
|
||||
partial(_trigger_generation, win, "img2img-basic", source_file, True)
|
||||
)
|
||||
parent_menu.addMenu(ai_menu)
|
||||
return ai_menu
|
||||
|
||||
elif media_type == "video":
|
||||
action = ai_menu.addAction(_("Increase Resolution (4x)"))
|
||||
action.setIcon(_icon("ai-action-upscale.svg"))
|
||||
action.triggered.connect(
|
||||
partial(_trigger_generation, win, "video-upscale-gan", source_file, False)
|
||||
)
|
||||
action = ai_menu.addAction(_("Smooth Motion (2x Frame Rate)"))
|
||||
action.setIcon(_icon("ai-action-smooth.svg"))
|
||||
action.triggered.connect(
|
||||
partial(_trigger_generation, win, "video-frame-interpolation-rife2x", source_file, False)
|
||||
)
|
||||
action = ai_menu.addAction(_("Split into Scenes"))
|
||||
action.setIcon(_icon("ai-action-scenes.svg"))
|
||||
action.triggered.connect(
|
||||
partial(_trigger_generation, win, "video-segment-scenes-transnet", source_file, False)
|
||||
)
|
||||
action = ai_menu.addAction(_("Add Captions from Speech"))
|
||||
action.setIcon(_icon("ai-action-captions.svg"))
|
||||
action.triggered.connect(
|
||||
partial(_trigger_generation, win, "video-whisper-srt", source_file, False)
|
||||
)
|
||||
ai_menu.addSeparator()
|
||||
action = ai_menu.addAction(_("Change Video Style..."))
|
||||
action.setIcon(_icon("ai-action-restyle.svg"))
|
||||
action.triggered.connect(
|
||||
partial(_trigger_generation, win, "video2video-basic", source_file, True)
|
||||
)
|
||||
else:
|
||||
action = ai_menu.addAction(_("No AI enhancement actions available yet."))
|
||||
action.setEnabled(False)
|
||||
|
||||
parent_menu.addMenu(ai_menu)
|
||||
return ai_menu
|
||||
|
||||
ai_menu = StyledContextMenu(title=_("Create with AI"), parent=parent_menu)
|
||||
ai_menu.setIcon(_icon("tool-generate-sparkle.svg"))
|
||||
action = ai_menu.addAction(_("Image..."))
|
||||
action.setIcon(_icon("ai-action-create-image.svg"))
|
||||
action.triggered.connect(partial(_trigger_generation, win, "txt2img-basic", source_file, True))
|
||||
action = ai_menu.addAction(_("Video..."))
|
||||
action.setIcon(_icon("ai-action-create-video.svg"))
|
||||
action.triggered.connect(partial(_trigger_generation, win, "txt2video-svd", source_file, True))
|
||||
action = ai_menu.addAction(_("Audio..."))
|
||||
action.setIcon(_icon("ai-action-create-audio.svg"))
|
||||
action.triggered.connect(partial(_trigger_generation, win, "txt2audio-stable-open", source_file, True))
|
||||
|
||||
parent_menu.addMenu(ai_menu)
|
||||
return ai_menu
|
||||
@@ -29,14 +29,15 @@
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from PyQt5.QtCore import QSize, Qt, QPoint, QRegExp
|
||||
from PyQt5.QtGui import QDrag, QCursor, QPixmap, QPainter, QIcon, QColor
|
||||
from PyQt5.QtCore import QSize, Qt, QPoint, QRegExp, QItemSelectionModel
|
||||
from PyQt5.QtGui import QDrag, QCursor, QPixmap, QPainter, QIcon, QColor, QFontMetrics
|
||||
from PyQt5.QtWidgets import QListView, QAbstractItemView, QStyledItemDelegate, QStyleOptionViewItem, QStyle
|
||||
|
||||
from classes import info
|
||||
from classes.app import get_app
|
||||
from classes.logger import log
|
||||
from classes.query import File
|
||||
from .ai_tools_menu import add_ai_tools_menu
|
||||
from .menu import StyledContextMenu
|
||||
|
||||
|
||||
@@ -120,9 +121,21 @@ class FilesListProgressDelegate(QStyledItemDelegate):
|
||||
painter.setBrush(QColor("#53A0ED"))
|
||||
painter.drawRect(fill_rect)
|
||||
if status == "queued":
|
||||
text_rect = full_rect.adjusted(0, -14, 0, -4)
|
||||
painter.setPen(QColor("#9EC8F7"))
|
||||
painter.drawText(text_rect, Qt.AlignLeft | Qt.AlignVCenter, "Queued")
|
||||
label = "Queued"
|
||||
fm = QFontMetrics(painter.font())
|
||||
text_w = fm.horizontalAdvance(label)
|
||||
text_h = fm.height()
|
||||
pad_x = 5
|
||||
pad_y = 2
|
||||
badge_w = text_w + (pad_x * 2)
|
||||
badge_h = text_h + (pad_y * 2)
|
||||
badge_rect = deco_rect.adjusted(3, 3, 0, 0)
|
||||
badge_rect.setWidth(badge_w)
|
||||
badge_rect.setHeight(badge_h)
|
||||
painter.setBrush(QColor(18, 22, 30, 220))
|
||||
painter.drawRoundedRect(badge_rect, 4, 4)
|
||||
painter.setPen(QColor("#EAF5FF"))
|
||||
painter.drawText(badge_rect, Qt.AlignCenter, label)
|
||||
painter.restore()
|
||||
|
||||
|
||||
@@ -140,12 +153,20 @@ class FilesListView(QListView):
|
||||
app.context_menu_object = "files"
|
||||
|
||||
index = self.indexAt(event.pos())
|
||||
if index.isValid():
|
||||
self.setCurrentIndex(index)
|
||||
self.selectionModel().select(
|
||||
index,
|
||||
QItemSelectionModel.ClearAndSelect,
|
||||
)
|
||||
|
||||
# Build menu
|
||||
menu = StyledContextMenu(parent=self)
|
||||
|
||||
menu.addAction(self.win.actionImportFiles)
|
||||
|
||||
source_file = None
|
||||
|
||||
active_job = None
|
||||
file_id = None
|
||||
if index.isValid():
|
||||
@@ -161,9 +182,11 @@ class FilesListView(QListView):
|
||||
active_job = None
|
||||
else:
|
||||
active_job = self.win.active_generation_job_for_file(file_id)
|
||||
source_file = File.get(id=file_id)
|
||||
add_ai_tools_menu(self.win, menu, source_file=source_file)
|
||||
|
||||
if not active_job:
|
||||
self.win.actionGenerate.setEnabled(self.win.can_open_generate_dialog())
|
||||
menu.addAction(self.win.actionGenerate)
|
||||
if active_job:
|
||||
cancel_action = menu.addAction(_("Cancel Job"))
|
||||
delete_icon_path = os.path.join(info.PATH, "themes", "cosmic", "images", "track-delete-enabled.svg")
|
||||
@@ -232,6 +255,13 @@ class FilesListView(QListView):
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
super(FilesListView, self).mouseDoubleClickEvent(event)
|
||||
index = self.indexAt(event.pos())
|
||||
if index.isValid():
|
||||
self.setCurrentIndex(index)
|
||||
self.selectionModel().select(
|
||||
index,
|
||||
QItemSelectionModel.ClearAndSelect,
|
||||
)
|
||||
# Preview File, File Properties, or Split File (depending on Shift/Ctrl)
|
||||
if int(get_app().keyboardModifiers() & Qt.ShiftModifier) > 0:
|
||||
get_app().window.actionSplitFile.trigger()
|
||||
|
||||
@@ -31,13 +31,14 @@ import os
|
||||
import uuid
|
||||
|
||||
from PyQt5.QtCore import QSize, Qt, QPoint
|
||||
from PyQt5.QtGui import QDrag, QCursor, QPixmap, QPainter, QIcon, QColor
|
||||
from PyQt5.QtGui import QDrag, QCursor, QPixmap, QPainter, QIcon, QColor, QFontMetrics
|
||||
from PyQt5.QtWidgets import QTreeView, QAbstractItemView, QSizePolicy, QHeaderView, QStyledItemDelegate, QStyleOptionViewItem, QStyle
|
||||
|
||||
from classes import info
|
||||
from classes.app import get_app
|
||||
from classes.logger import log
|
||||
from classes.query import File
|
||||
from .ai_tools_menu import add_ai_tools_menu
|
||||
from .menu import StyledContextMenu
|
||||
|
||||
|
||||
@@ -116,9 +117,21 @@ class FilesTreeProgressDelegate(QStyledItemDelegate):
|
||||
painter.setBrush(QColor("#53A0ED"))
|
||||
painter.drawRect(fill_rect)
|
||||
if status == "queued":
|
||||
text_rect = full_rect.adjusted(0, -14, 0, -4)
|
||||
painter.setPen(QColor("#9EC8F7"))
|
||||
painter.drawText(text_rect, Qt.AlignLeft | Qt.AlignVCenter, "Queued")
|
||||
label = "Queued"
|
||||
fm = QFontMetrics(painter.font())
|
||||
text_w = fm.horizontalAdvance(label)
|
||||
text_h = fm.height()
|
||||
pad_x = 5
|
||||
pad_y = 2
|
||||
badge_w = text_w + (pad_x * 2)
|
||||
badge_h = text_h + (pad_y * 2)
|
||||
badge_rect = deco_rect.adjusted(3, 3, 0, 0)
|
||||
badge_rect.setWidth(badge_w)
|
||||
badge_rect.setHeight(badge_h)
|
||||
painter.setBrush(QColor(18, 22, 30, 220))
|
||||
painter.drawRoundedRect(badge_rect, 4, 4)
|
||||
painter.setPen(QColor("#EAF5FF"))
|
||||
painter.drawText(badge_rect, Qt.AlignCenter, label)
|
||||
painter.restore()
|
||||
|
||||
|
||||
@@ -142,6 +155,7 @@ class FilesTreeView(QTreeView):
|
||||
|
||||
menu.addAction(self.win.actionImportFiles)
|
||||
|
||||
source_file = None
|
||||
active_job = None
|
||||
file_id = None
|
||||
if index.isValid():
|
||||
@@ -155,9 +169,11 @@ class FilesTreeView(QTreeView):
|
||||
active_job = None
|
||||
else:
|
||||
active_job = self.win.active_generation_job_for_file(file_id)
|
||||
source_file = File.get(id=file_id)
|
||||
add_ai_tools_menu(self.win, menu, source_file=source_file)
|
||||
|
||||
if not active_job:
|
||||
self.win.actionGenerate.setEnabled(self.win.can_open_generate_dialog())
|
||||
menu.addAction(self.win.actionGenerate)
|
||||
if active_job:
|
||||
cancel_action = menu.addAction(_("Cancel Job"))
|
||||
delete_icon_path = os.path.join(info.PATH, "themes", "cosmic", "images", "track-delete-enabled.svg")
|
||||
|
||||
@@ -1057,7 +1057,7 @@ class TimelineView(updates.UpdateInterface, ViewClass):
|
||||
if not has_clipboard and not found_gap:
|
||||
return
|
||||
|
||||
# Get track object (ignore locked tracks)
|
||||
# Get track object (ignore locked tracks for edit operations)
|
||||
track = Track.get(number=layer_number)
|
||||
if not track:
|
||||
return
|
||||
@@ -1068,6 +1068,8 @@ class TimelineView(updates.UpdateInterface, ViewClass):
|
||||
# New context menu
|
||||
menu = StyledContextMenu(parent=self)
|
||||
|
||||
has_edit_actions = False
|
||||
|
||||
if found_gap:
|
||||
# Add 'Remove Gap' Menu
|
||||
menu.addAction(self.window.actionRemoveGap)
|
||||
@@ -1079,8 +1081,7 @@ class TimelineView(updates.UpdateInterface, ViewClass):
|
||||
self.window.actionRemoveGap.triggered.connect(
|
||||
partial(self.RemoveGap_Triggered, found_start, found_end, int(layer_number))
|
||||
)
|
||||
if has_clipboard and found_gap:
|
||||
menu.addSeparator()
|
||||
has_edit_actions = True
|
||||
if has_clipboard:
|
||||
# Add 'Paste' Menu
|
||||
Paste_Clip = menu.addAction(_("Paste"))
|
||||
@@ -1088,6 +1089,7 @@ class TimelineView(updates.UpdateInterface, ViewClass):
|
||||
Paste_Clip.triggered.connect(
|
||||
partial(self.Paste_Triggered, MenuCopy.PASTE, [], [])
|
||||
)
|
||||
has_edit_actions = True
|
||||
|
||||
# Show context menu
|
||||
self.context_menu_cursor_position = QCursor.pos()
|
||||
|
||||