565 lines
22 KiB
Python
565 lines
22 KiB
Python
"""
|
||
library_panel.py — Musikbibliotek med søgning og drag-and-drop til danseliste.
|
||
"""
|
||
|
||
from PyQt6.QtWidgets import (
|
||
QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
|
||
QLineEdit, QLabel, QHBoxLayout, QPushButton,
|
||
QFrame, QSlider, QCheckBox,
|
||
QAbstractItemView, QStyledItemDelegate,
|
||
)
|
||
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QMimeData, QByteArray, QRect
|
||
from PyQt6.QtGui import QColor, QDrag, QPainter, QBrush, QPen, QFont
|
||
|
||
|
||
class DanseButtonDelegate(QStyledItemDelegate):
|
||
"""Tegner en orange 'Danse' label i højre side af hvert list-item."""
|
||
|
||
BTN_W = 54
|
||
BTN_H = 22
|
||
BTN_MARGIN = 8
|
||
|
||
def paint(self, painter: QPainter, option, index):
|
||
super().paint(painter, option, index)
|
||
rect = option.rect
|
||
btn_rect = QRect(
|
||
rect.right() - self.BTN_W - self.BTN_MARGIN,
|
||
rect.top() + (rect.height() - self.BTN_H) // 2,
|
||
self.BTN_W,
|
||
self.BTN_H,
|
||
)
|
||
painter.save()
|
||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||
painter.setBrush(QBrush(QColor("#e8a020")))
|
||
painter.setPen(Qt.PenStyle.NoPen)
|
||
painter.drawRoundedRect(btn_rect, 4, 4)
|
||
painter.setPen(QPen(QColor("#111111")))
|
||
font = QFont()
|
||
font.setPointSize(8)
|
||
font.setBold(True)
|
||
painter.setFont(font)
|
||
painter.drawText(btn_rect, Qt.AlignmentFlag.AlignCenter, "Danse")
|
||
painter.restore()
|
||
|
||
|
||
class DraggableLibraryList(QListWidget):
|
||
"""QListWidget med drag, dobbeltklik og klik på højre side for dans-tags."""
|
||
|
||
danse_clicked = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal(dict)
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.setDragEnabled(True)
|
||
self.setDragDropMode(QAbstractItemView.DragDropMode.DragOnly)
|
||
self.setDefaultDropAction(Qt.DropAction.CopyAction)
|
||
|
||
def mousePressEvent(self, event):
|
||
if event.button() == Qt.MouseButton.LeftButton:
|
||
item = self.itemAt(event.pos())
|
||
if item and event.pos().x() > self.viewport().width() - 75:
|
||
song = item.data(Qt.ItemDataRole.UserRole)
|
||
if song:
|
||
self.danse_clicked.emit(song)
|
||
return
|
||
super().mousePressEvent(event)
|
||
|
||
def mouseDoubleClickEvent(self, event):
|
||
if event.button() == Qt.MouseButton.LeftButton:
|
||
item = self.itemAt(event.pos())
|
||
if item:
|
||
# Dobbeltklik i højre 75px = Danse, ellers song_selected
|
||
if event.pos().x() > self.viewport().width() - 75:
|
||
song = item.data(Qt.ItemDataRole.UserRole)
|
||
if song:
|
||
self.danse_clicked.emit(song)
|
||
return
|
||
super().mouseDoubleClickEvent(event)
|
||
|
||
def startDrag(self, supported_actions):
|
||
item = self.currentItem()
|
||
if not item:
|
||
return
|
||
song = item.data(Qt.ItemDataRole.UserRole)
|
||
if not song:
|
||
return
|
||
|
||
import json
|
||
data = json.dumps(song).encode("utf-8")
|
||
|
||
mime = QMimeData()
|
||
mime.setData("application/x-linedance-song", QByteArray(data))
|
||
mime.setText(song.get("title", ""))
|
||
|
||
drag = QDrag(self)
|
||
drag.setMimeData(mime)
|
||
drag.exec(Qt.DropAction.CopyAction)
|
||
|
||
|
||
class LibraryPanel(QWidget):
|
||
song_selected = pyqtSignal(dict)
|
||
add_to_playlist = pyqtSignal(dict)
|
||
scan_requested = pyqtSignal()
|
||
edit_tags_requested = pyqtSignal(dict)
|
||
send_mail_requested = pyqtSignal(dict)
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self._all_songs: list[dict] = []
|
||
self._filtered: list[dict] = []
|
||
self._bpm_scan_running = False
|
||
self._search_timer = QTimer(self)
|
||
self._search_timer.setSingleShot(True)
|
||
self._search_timer.setInterval(150)
|
||
self._search_timer.timeout.connect(self._do_search)
|
||
self._build_ui()
|
||
|
||
def _build_ui(self):
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
layout.setSpacing(0)
|
||
|
||
# Header
|
||
header = QHBoxLayout()
|
||
header.setContentsMargins(10, 6, 10, 6)
|
||
lbl = QLabel("BIBLIOTEK")
|
||
lbl.setObjectName("section_title")
|
||
header.addWidget(lbl)
|
||
header.addStretch()
|
||
|
||
btn_refresh = QPushButton("↻ Opdater")
|
||
btn_refresh.setFixedHeight(28)
|
||
btn_refresh.setToolTip("Opdater bibliotek fra database")
|
||
btn_refresh.clicked.connect(self._refresh_library)
|
||
header.addWidget(btn_refresh)
|
||
|
||
btn_manage = QPushButton("⚙ Mapper")
|
||
btn_manage.setFixedHeight(28)
|
||
btn_manage.setToolTip("Tilføj, fjern og scan musikbiblioteker")
|
||
btn_manage.clicked.connect(self._manage_libraries)
|
||
header.addWidget(btn_manage)
|
||
layout.addLayout(header)
|
||
|
||
# Søgefelt + checkbox
|
||
search_row = QHBoxLayout()
|
||
search_row.setSpacing(6)
|
||
self._search = QLineEdit()
|
||
self._search.setPlaceholderText("Søg i titel, artist, album, dans...")
|
||
self._search.textChanged.connect(self._on_search_changed)
|
||
search_row.addWidget(self._search)
|
||
from PyQt6.QtWidgets import QCheckBox
|
||
self._chk_alt = QCheckBox("Inkl. alt.")
|
||
self._chk_alt.setToolTip("Søg også i alternativ-danse")
|
||
self._chk_alt.setChecked(False)
|
||
self._chk_alt.toggled.connect(self._on_search_changed)
|
||
search_row.addWidget(self._chk_alt)
|
||
layout.addLayout(search_row)
|
||
|
||
# Resultat-tæller + drag-hint
|
||
hint_row = QHBoxLayout()
|
||
hint_row.setContentsMargins(8, 2, 8, 2)
|
||
self._count_label = QLabel("0 sange")
|
||
self._count_label.setObjectName("result_count")
|
||
hint_row.addWidget(self._count_label)
|
||
hint_row.addStretch()
|
||
drag_hint = QLabel("træk til danseliste →")
|
||
drag_hint.setObjectName("result_count")
|
||
hint_row.addWidget(drag_hint)
|
||
layout.addLayout(hint_row)
|
||
|
||
# Liste — draggable
|
||
self._list = DraggableLibraryList()
|
||
self._list.setObjectName("library_list")
|
||
self._list.itemDoubleClicked.connect(self._on_double_click)
|
||
self._list.danse_clicked.connect(self.edit_tags_requested)
|
||
self._list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||
self._list.customContextMenuRequested.connect(self._show_context_menu)
|
||
self._list.setItemDelegate(DanseButtonDelegate(self._list))
|
||
layout.addWidget(self._list)
|
||
|
||
# Preview-afspiller bar
|
||
self._preview_bar = self._build_preview_bar()
|
||
self._preview_bar.setVisible(False)
|
||
layout.addWidget(self._preview_bar)
|
||
|
||
# Timer til preview progress opdatering
|
||
self._preview_timer = QTimer(self)
|
||
self._preview_timer.setInterval(200)
|
||
self._preview_timer.timeout.connect(self._update_preview_progress)
|
||
|
||
def _build_preview_bar(self) -> QWidget:
|
||
bar = QFrame()
|
||
bar.setObjectName("track_display")
|
||
bar.setFixedHeight(62)
|
||
row = QHBoxLayout(bar)
|
||
row.setContentsMargins(10, 8, 10, 8)
|
||
row.setSpacing(10)
|
||
|
||
# Play/pause knap — orange som hoved-afspiller
|
||
self._btn_preview_play = QPushButton("▶")
|
||
self._btn_preview_play.setFixedSize(36, 36)
|
||
self._btn_preview_play.setObjectName("btn_play_small")
|
||
self._btn_preview_play.setToolTip("Afspil / Pause")
|
||
self._btn_preview_play.clicked.connect(self._toggle_preview_playback)
|
||
row.addWidget(self._btn_preview_play)
|
||
|
||
# Stop knap
|
||
self._btn_preview_stop = QPushButton("■")
|
||
self._btn_preview_stop.setFixedSize(30, 30)
|
||
self._btn_preview_stop.setObjectName("btn_stop_small")
|
||
self._btn_preview_stop.setToolTip("Stop preview")
|
||
self._btn_preview_stop.clicked.connect(self._stop_preview)
|
||
row.addWidget(self._btn_preview_stop)
|
||
|
||
# Titel + progress i midten
|
||
info = QVBoxLayout()
|
||
info.setSpacing(4)
|
||
info.setContentsMargins(0, 0, 0, 0)
|
||
|
||
self._lbl_preview_title = QLabel("—")
|
||
self._lbl_preview_title.setObjectName("track_meta")
|
||
info.addWidget(self._lbl_preview_title)
|
||
|
||
self._preview_progress = QSlider(Qt.Orientation.Horizontal)
|
||
self._preview_progress.setRange(0, 1000)
|
||
self._preview_progress.sliderMoved.connect(self._seek_preview)
|
||
info.addWidget(self._preview_progress)
|
||
row.addLayout(info, stretch=1)
|
||
|
||
# Tid
|
||
self._lbl_preview_time = QLabel("0:00")
|
||
self._lbl_preview_time.setObjectName("track_meta")
|
||
self._lbl_preview_time.setFixedWidth(70)
|
||
self._lbl_preview_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||
row.addWidget(self._lbl_preview_time)
|
||
|
||
# Volumen
|
||
self._preview_vol = QSlider(Qt.Orientation.Horizontal)
|
||
self._preview_vol.setRange(0, 100)
|
||
self._preview_vol.setValue(78)
|
||
self._preview_vol.setFixedWidth(70)
|
||
self._preview_vol.setToolTip("Volumen preview")
|
||
self._preview_vol.valueChanged.connect(self._on_preview_volume)
|
||
row.addWidget(self._preview_vol)
|
||
|
||
return bar
|
||
|
||
# ── Scanning ──────────────────────────────────────────────────────────────
|
||
|
||
def _on_scan_clicked(self):
|
||
self.scan_requested.emit()
|
||
|
||
def set_scanning(self, scanning: bool, status_text: str = ""):
|
||
pass # Status vises i statuslinjen
|
||
|
||
def update_scan_status(self, text: str):
|
||
pass # Status vises i statuslinjen
|
||
|
||
# ── Sange ─────────────────────────────────────────────────────────────────
|
||
|
||
def load_songs(self, songs: list[dict]):
|
||
self._all_songs = songs
|
||
self._do_search()
|
||
|
||
# ── Søgning ───────────────────────────────────────────────────────────────
|
||
|
||
def _on_search_changed(self):
|
||
self._search_timer.start()
|
||
|
||
def _do_search(self):
|
||
q = self._search.text().strip().lower()
|
||
incl_alt = self._chk_alt.isChecked()
|
||
self._filtered = [
|
||
s for s in self._all_songs if self._matches(s, q, incl_alt)
|
||
] if q else list(self._all_songs)
|
||
total = len(self._all_songs)
|
||
found = len(self._filtered)
|
||
q_text = self._search.text().strip()
|
||
self._count_label.setText(
|
||
f"{found} resultat{'er' if found != 1 else ''} for \"{q_text}\"" if q_text
|
||
else f"{total} sang{'e' if total != 1 else ''}"
|
||
)
|
||
self._render()
|
||
|
||
def _matches(self, song: dict, q: str, incl_alt: bool = False) -> bool:
|
||
fields = [
|
||
song.get("title", ""), song.get("artist", ""),
|
||
song.get("album", ""), song.get("file_format", ""),
|
||
] + song.get("dances", [])
|
||
if incl_alt:
|
||
fields += song.get("alt_dances", [])
|
||
return any(q in f.lower() for f in fields)
|
||
|
||
def _render(self):
|
||
self._list.clear()
|
||
q = self._search.text().strip().lower()
|
||
for song in self._filtered:
|
||
dances = song.get("dances", [])
|
||
dance_levels = song.get("dance_levels", [])
|
||
missing = song.get("file_missing", False)
|
||
|
||
dance_parts = []
|
||
for i, d in enumerate(dances):
|
||
lvl = dance_levels[i] if i < len(dance_levels) else ""
|
||
dance_parts.append(f"{d} / {lvl}" if lvl else d)
|
||
dance_str = " · " + " | ".join(dance_parts) if dance_parts else ""
|
||
|
||
prefix = "⚠ " if missing else ""
|
||
bpm = song.get("bpm", 0)
|
||
bpm_str = f"{bpm} BPM" if bpm else "? BPM"
|
||
line1 = prefix + song.get("title", "—")
|
||
line2 = f" {song.get('artist','—')} · {bpm_str} · {song.get('file_format','').upper()}{dance_str}"
|
||
|
||
item = QListWidgetItem(f"{line1}\n{line2}")
|
||
item.setData(Qt.ItemDataRole.UserRole, song)
|
||
item.setSizeHint(__import__('PyQt6.QtCore', fromlist=['QSize']).QSize(0, 52))
|
||
if missing:
|
||
item.setForeground(QColor("#5a6070"))
|
||
elif q and any(q in d.lower() for d in dances):
|
||
item.setForeground(QColor("#e8a020"))
|
||
self._list.addItem(item)
|
||
|
||
def _start_bulk_bpm_scan(self):
|
||
"""Start BPM-analyse på alle sange uden BPM i baggrundstråd med lav prioritet."""
|
||
if self._bpm_scan_running:
|
||
return
|
||
songs_without_bpm = [s for s in self._all_songs
|
||
if not s.get("bpm") and not s.get("file_missing")]
|
||
if not songs_without_bpm:
|
||
self._btn_bpm_scan.setText("♩ Alle har BPM")
|
||
return
|
||
|
||
self._bpm_scan_running = True
|
||
self._btn_bpm_scan.setText(f"♩ Scanner 0/{len(songs_without_bpm)}...")
|
||
self._btn_bpm_scan.setEnabled(False)
|
||
|
||
from PyQt6.QtCore import QThread, pyqtSignal as _sig
|
||
|
||
class BulkBpmWorker(QThread):
|
||
progress = _sig(int, int, str) # done, total, title
|
||
finished = _sig()
|
||
|
||
def __init__(self, songs):
|
||
super().__init__()
|
||
self._songs = songs
|
||
|
||
def run(self):
|
||
from local.tag_reader import analyze_and_save_bpm
|
||
total = len(self._songs)
|
||
for i, song in enumerate(self._songs, start=1):
|
||
if self.isInterruptionRequested():
|
||
break
|
||
try:
|
||
bpm = analyze_and_save_bpm(song["local_path"], song["id"])
|
||
if bpm:
|
||
song["bpm"] = int(round(bpm))
|
||
except Exception:
|
||
pass
|
||
self.progress.emit(i, total, song.get("title", ""))
|
||
self.finished.emit()
|
||
|
||
self._bulk_bpm_worker = BulkBpmWorker(songs_without_bpm)
|
||
|
||
def on_progress(done, total, title):
|
||
self._btn_bpm_scan.setText(f"♩ {done}/{total}...")
|
||
# Opdater sangen i listen
|
||
for s in self._all_songs:
|
||
if s.get("title") == title and s.get("bpm"):
|
||
break
|
||
self._do_search()
|
||
|
||
def on_finished():
|
||
self._bpm_scan_running = False
|
||
self._btn_bpm_scan.setEnabled(True)
|
||
self._btn_bpm_scan.setText("♩ BPM alle")
|
||
self._do_search()
|
||
|
||
self._bulk_bpm_worker.progress.connect(on_progress)
|
||
self._bulk_bpm_worker.finished.connect(on_finished)
|
||
self._bulk_bpm_worker.start()
|
||
self._bulk_bpm_worker.setPriority(QThread.Priority.LowestPriority)
|
||
|
||
# ── Handlinger ────────────────────────────────────────────────────────────
|
||
|
||
def _show_context_menu(self, pos):
|
||
from PyQt6.QtWidgets import QMenu
|
||
item = self._list.itemAt(pos)
|
||
if not item:
|
||
return
|
||
song = item.data(Qt.ItemDataRole.UserRole)
|
||
if not song:
|
||
return
|
||
menu = QMenu(self)
|
||
act_add = menu.addAction("Tilføj til danseliste")
|
||
act_play = menu.addAction("Afspil")
|
||
# Preview kun hvis preview player er sat op
|
||
is_previewing = (
|
||
hasattr(self, "_preview_player") and
|
||
self._preview_player and
|
||
self._preview_player.is_playing()
|
||
)
|
||
act_preview = menu.addAction(
|
||
"⏹ Stop preview" if is_previewing else "▶ Preview (høretelefoner)"
|
||
)
|
||
menu.addSeparator()
|
||
act_tags = menu.addAction("✎ Rediger dans-tags...")
|
||
act_info = menu.addAction("ℹ Dans-info...")
|
||
act_bpm = menu.addAction("♩ Analysér BPM")
|
||
menu.addSeparator()
|
||
send_menu = menu.addMenu("Send til")
|
||
act_mail = send_menu.addAction("✉ Send som mail")
|
||
action = menu.exec(self._list.mapToGlobal(pos))
|
||
if action == act_add:
|
||
self.add_to_playlist.emit(song)
|
||
elif action == act_play:
|
||
self.song_selected.emit(song)
|
||
elif action == act_preview:
|
||
self._toggle_preview(song)
|
||
elif action == act_tags:
|
||
self.edit_tags_requested.emit(song)
|
||
elif action == act_info:
|
||
from ui.dance_info_dialog import DanceInfoDialog
|
||
dlg = DanceInfoDialog(song, parent=self.window())
|
||
dlg.exec()
|
||
elif action == act_bpm:
|
||
self._analyze_bpm(song)
|
||
elif action == act_mail:
|
||
self.send_mail_requested.emit(song)
|
||
|
||
def set_preview_player(self, preview_player):
|
||
"""Sæt preview-afspilleren fra main_window."""
|
||
self._preview_player = preview_player
|
||
|
||
def _on_double_click(self, item: QListWidgetItem):
|
||
song = item.data(Qt.ItemDataRole.UserRole)
|
||
if song:
|
||
self._start_preview(song)
|
||
|
||
def _start_preview(self, song: dict):
|
||
if not hasattr(self, "_preview_player") or not self._preview_player:
|
||
self.song_selected.emit(song)
|
||
return
|
||
path = song.get("local_path", "")
|
||
if not path:
|
||
return
|
||
self._preview_song = song
|
||
self._preview_player.play(path)
|
||
title = song.get("title", "—")
|
||
artist = song.get("artist", "")
|
||
self._lbl_preview_title.setText(f"{title} · {artist}")
|
||
self._btn_preview_play.setText("⏸")
|
||
self._preview_bar.setVisible(True)
|
||
self._preview_timer.start()
|
||
|
||
def _toggle_preview_playback(self):
|
||
if not hasattr(self, "_preview_player") or not self._preview_player:
|
||
return
|
||
if self._preview_player.is_playing():
|
||
self._preview_player.pause()
|
||
self._btn_preview_play.setText("▶")
|
||
else:
|
||
self._preview_player.resume()
|
||
self._btn_preview_play.setText("⏸")
|
||
|
||
def _stop_preview(self):
|
||
if hasattr(self, "_preview_player") and self._preview_player:
|
||
self._preview_player.stop()
|
||
self._preview_timer.stop()
|
||
self._preview_bar.setVisible(False)
|
||
self._btn_preview_play.setText("▶")
|
||
|
||
def _seek_preview(self, value: int):
|
||
if hasattr(self, "_preview_player") and self._preview_player:
|
||
self._preview_player.seek(value / 1000.0)
|
||
|
||
def _on_preview_volume(self, value: int):
|
||
if hasattr(self, "_preview_player") and self._preview_player:
|
||
self._preview_player.set_volume(value)
|
||
|
||
def _update_preview_progress(self):
|
||
if not hasattr(self, "_preview_player") or not self._preview_player:
|
||
return
|
||
if not self._preview_player.is_playing():
|
||
self._btn_preview_play.setText("▶")
|
||
return
|
||
pos = self._preview_player.get_position()
|
||
cur = self._preview_player.get_time()
|
||
dur = self._preview_player.get_duration()
|
||
self._preview_progress.setValue(int(pos * 1000))
|
||
def fmt(s): return f"{s//60}:{s%60:02d}"
|
||
self._lbl_preview_time.setText(f"{fmt(cur)} / {fmt(dur)}")
|
||
|
||
def _toggle_preview(self, song: dict):
|
||
"""Start/stop preview af en sang."""
|
||
if not hasattr(self, "_preview_player") or not self._preview_player:
|
||
return
|
||
if self._preview_player.is_playing():
|
||
self._stop_preview()
|
||
else:
|
||
self._start_preview(song)
|
||
|
||
def _analyze_bpm(self, song: dict):
|
||
"""Analysér BPM i baggrundstråd og opdater biblioteket."""
|
||
path = song.get("local_path", "")
|
||
song_id = song.get("id", "")
|
||
if not path or not song_id:
|
||
return
|
||
from PyQt6.QtCore import QThread, pyqtSignal as _sig
|
||
|
||
class BpmWorker(QThread):
|
||
done = _sig(float)
|
||
def __init__(self, p, sid):
|
||
super().__init__()
|
||
self._p, self._sid = p, sid
|
||
def run(self):
|
||
from local.tag_reader import analyze_and_save_bpm
|
||
bpm = analyze_and_save_bpm(self._p, self._sid)
|
||
if bpm:
|
||
self.done.emit(bpm)
|
||
|
||
self._bpm_worker = BpmWorker(path, song_id)
|
||
|
||
def on_bpm_done(bpm):
|
||
# Opdater sangen i _all_songs listen direkte
|
||
for s in self._all_songs:
|
||
if s.get("id") == song_id:
|
||
s["bpm"] = int(round(bpm))
|
||
break
|
||
self._do_search()
|
||
|
||
self._bpm_worker.done.connect(on_bpm_done)
|
||
self._bpm_worker.start()
|
||
|
||
def _refresh_library(self):
|
||
"""Genindlæs bibliotek fra database."""
|
||
mw = self.window()
|
||
if hasattr(mw, "_reload_library"):
|
||
mw._reload_library()
|
||
|
||
def _manage_libraries(self):
|
||
from ui.library_manager import LibraryManagerDialog
|
||
from local.local_db import DB_PATH
|
||
dialog = LibraryManagerDialog(db_path=str(DB_PATH), parent=self.window())
|
||
dialog.libraries_changed.connect(self._on_libraries_changed)
|
||
dialog.exec()
|
||
# Reload øjeblikkeligt når dialog lukkes
|
||
mw = self.window()
|
||
if hasattr(mw, "_reload_library"):
|
||
mw._reload_library()
|
||
# Start scanning
|
||
if hasattr(mw, "start_background_scan"):
|
||
QTimer.singleShot(1000, mw.start_background_scan)
|
||
|
||
def _on_libraries_changed(self):
|
||
"""Kaldes ved tilføj/fjern — reload øjeblikkeligt."""
|
||
mw = self.window()
|
||
if hasattr(mw, "_reload_library"):
|
||
mw._reload_library()
|
||
|
||
def _add_folder(self):
|
||
from PyQt6.QtWidgets import QFileDialog
|
||
folder = QFileDialog.getExistingDirectory(self, "Vælg musikmappe")
|
||
if folder:
|
||
mw = self.window()
|
||
if hasattr(mw, "add_library_path"):
|
||
mw.add_library_path(folder)
|