diff --git a/tilia/requests/get.py b/tilia/requests/get.py
index ce3bc1e30..6d3fb9eb5 100644
--- a/tilia/requests/get.py
+++ b/tilia/requests/get.py
@@ -80,8 +80,8 @@ def get(request: Get, *args, **kwargs) -> Any:
try:
return _requests_to_callbacks[request](*args, **kwargs)
- except KeyError:
- raise NoReplyToRequest(f"{request} has no repliers attached.")
+ except KeyError as e:
+ raise NoReplyToRequest(f"{request} has no repliers attached.") from e
except Exception as exc:
raise Exception(
f"Exception when processing {request} with {args=}, {kwargs=}"
@@ -110,12 +110,12 @@ def server(request: Get) -> tuple[Any | None, Callable | None]:
def stop_serving(replier: Any, request: Get) -> None:
"""
- Detaches a calback from a request.
+ Detaches a callback from a request.
"""
try:
_requests_to_callbacks.pop(request)
- except KeyError:
- raise NoCallbackAttached()
+ except KeyError as e:
+ raise NoCallbackAttached() from e
_servers_to_requests[replier].remove(request)
if not _servers_to_requests[replier]:
diff --git a/tilia/requests/post.py b/tilia/requests/post.py
index d0ace8aa4..9f78aba5f 100644
--- a/tilia/requests/post.py
+++ b/tilia/requests/post.py
@@ -130,7 +130,7 @@ def post(post: Post, *args, **kwargs) -> None:
# Should be used only when a single listener is expected.
# If there are multiple listeners, the result of the last listener is returned.
result = None
- for listener, callback in _posts_to_listeners[post].copy().items():
+ for callback in _posts_to_listeners[post].copy().values():
result = callback(*args, **kwargs)
return result
diff --git a/tilia/settings.py b/tilia/settings.py
index 20361bc6f..373dc40c4 100644
--- a/tilia/settings.py
+++ b/tilia/settings.py
@@ -89,7 +89,7 @@ class SettingsManager(QObject):
}
def __init__(self):
- self._settings = QSettings(tilia.constants.APP_NAME, "Desktop Settings")
+ self._settings = QSettings(tilia.constants.APP_NAME, "Desktop Settings", None)
self._files_updated_callbacks = set()
self._cache = {}
self._check_all_default_settings_present()
@@ -150,8 +150,8 @@ def set(self, group_name: str, setting: str, value):
try:
self._cache[group_name][setting] = value
self._set(group_name, setting, value)
- except AttributeError:
- raise AttributeError(f"{group_name}.{setting} not found in cache.")
+ except AttributeError as e:
+ raise AttributeError(f"{group_name}.{setting} not found in cache.") from e
@staticmethod
def _get_key(group_name: str, setting: str, in_default: bool) -> str:
diff --git a/tilia/timelines/base/component/base.py b/tilia/timelines/base/component/base.py
index 707121f83..6e1b8e4f2 100644
--- a/tilia/timelines/base/component/base.py
+++ b/tilia/timelines/base/component/base.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-from abc import ABC
from typing import Any, Callable
from tilia.exceptions import SetComponentDataError, GetComponentDataError
@@ -10,7 +9,7 @@
from tilia.utils import get_tilia_class_string
-class TimelineComponent(ABC):
+class TimelineComponent:
SERIALIZABLE = []
ORDERING_ATTRS = tuple()
@@ -54,10 +53,10 @@ def validate_set_data(self, attr, value):
)
try:
return self.validators[attr](value)
- except KeyError:
+ except KeyError as e:
raise KeyError(
f"{self} has no validator for attribute {attr}. Can't set to '{value}'."
- )
+ ) from e
def set_data(self, attr: str, value: Any):
if not self.validate_set_data(attr, value):
@@ -71,11 +70,11 @@ def set_data(self, attr: str, value: Any):
def get_data(self, attr: str):
try:
return getattr(self, attr)
- except AttributeError:
+ except AttributeError as e:
raise GetComponentDataError(
"AttributeError while getting data from component."
f"Does {type(self)} have a {attr} attribute?"
- )
+ ) from e
@classmethod
def validate_creation(cls, *args, **kwargs) -> tuple[bool, str]:
diff --git a/tilia/timelines/base/timeline.py b/tilia/timelines/base/timeline.py
index 0dc010837..2d4b65a83 100644
--- a/tilia/timelines/base/timeline.py
+++ b/tilia/timelines/base/timeline.py
@@ -164,10 +164,10 @@ def validate_set_data(self, attr, value):
)
try:
return self.validators[attr](value)
- except KeyError:
+ except KeyError as e:
raise KeyError(
f"{self} has no validator for attribute {attr}. Can't set to '{value}'."
- )
+ ) from e
def set_data(self, attr: str, value: Any):
if not self.validate_set_data(attr, value):
@@ -470,11 +470,11 @@ def _remove_from_components_set(self, component: TC) -> None:
try:
self._components.remove(component)
self.id_to_component.pop(component.id)
- except KeyError:
+ except KeyError as e:
raise KeyError(
f"Can't remove component '{component}' from {self}: not in"
" self.components."
- )
+ ) from e
def update_component_order(self, component: TC):
self._components.remove(component)
diff --git a/tilia/timelines/beat/timeline.py b/tilia/timelines/beat/timeline.py
index a954058b9..fa13f3f6b 100644
--- a/tilia/timelines/beat/timeline.py
+++ b/tilia/timelines/beat/timeline.py
@@ -577,7 +577,7 @@ def get_beat_index(self, beat: Beat) -> int:
return self.components.index(beat)
def propagate_measure_number_change(self, start_index: int):
- for j, measure in enumerate(self.measure_numbers[start_index + 1 :]):
+ for j in range(len(self.measure_numbers[start_index + 1 :])):
propagate_index = j + start_index + 1
if propagate_index in self.measures_to_force_display:
break
diff --git a/tilia/timelines/collection/collection.py b/tilia/timelines/collection/collection.py
index 36cb62cf1..962f13dc3 100644
--- a/tilia/timelines/collection/collection.py
+++ b/tilia/timelines/collection/collection.py
@@ -86,10 +86,10 @@ def _validate_timeline_kind(kind: TlKind | str):
if isinstance(kind, str):
try:
kind = TlKind(kind)
- except ValueError:
+ except ValueError as e:
raise TimelineValidationError(
f"Can't create timeline: invalid timeline kind '{kind}'"
- )
+ ) from e
if not isinstance(kind, TlKind):
raise TimelineValidationError(
f"Can't create timeline: invalid timeline kind '{kind}'"
@@ -204,11 +204,11 @@ def _remove_from_timelines(self, timeline: Timeline) -> None:
for tl in self:
if tl.ordinal > timeline.ordinal:
tl.ordinal -= 1
- except ValueError:
+ except ValueError as e:
raise ValueError(
f"Can't remove timeline '{timeline}' from {self}: not in"
" self._timelines."
- )
+ ) from e
def get_export_data(self):
return [
diff --git a/tilia/timelines/harmony/components/harmony.py b/tilia/timelines/harmony/components/harmony.py
index f6ad9d102..182925e39 100644
--- a/tilia/timelines/harmony/components/harmony.py
+++ b/tilia/timelines/harmony/components/harmony.py
@@ -103,9 +103,11 @@ def __repr__(self):
@classmethod
def from_string(
- cls, time: float, string: str, key: music21.key.Key = music21.key.Key("C")
+ cls, time: float, string: str, key: music21.key.Key | str = "C major"
):
- music21_object, object_type = _get_music21_object_from_text(string, key)
+ music21_object, object_type = _get_music21_object_from_text(
+ string, key.__str__()
+ )
if not string:
return None
diff --git a/tilia/timelines/hierarchy/timeline.py b/tilia/timelines/hierarchy/timeline.py
index 68797c1b9..d4422f16d 100644
--- a/tilia/timelines/hierarchy/timeline.py
+++ b/tilia/timelines/hierarchy/timeline.py
@@ -95,7 +95,7 @@ def get_boundary_conflicts(self) -> list[tuple[Hierarchy, Hierarchy]]:
and hrc1.end == hrc2.end
and hrc1.level == hrc2.level
):
- # if hierachies have same times and level, there's a conflict
+ # if hierarchies have same times and level, there's a conflict
conflicts.append((hrc1, hrc2))
return conflicts
diff --git a/tilia/timelines/slider/timeline.py b/tilia/timelines/slider/timeline.py
index 49fd61560..02c5a296e 100644
--- a/tilia/timelines/slider/timeline.py
+++ b/tilia/timelines/slider/timeline.py
@@ -32,7 +32,7 @@ def is_empty(self):
return True
def _validate_delete_components(self, component: TimelineComponent):
- """Nothing to do. Must impement abstract method."""
+ """Nothing to do. Must implement abstract method."""
def get_state(self) -> dict:
result = {}
diff --git a/tilia/ui/cli/generate_scripts.py b/tilia/ui/cli/generate_scripts.py
index f5c393ab1..6e5b5f3c7 100644
--- a/tilia/ui/cli/generate_scripts.py
+++ b/tilia/ui/cli/generate_scripts.py
@@ -218,7 +218,7 @@ def get_scripts(directory: str | Path) -> list[Path]:
List of paths to individual scripts (str): The absolute path to each script.
"""
saved_scripts = []
- for folder_name, sub_folders, filenames in os.walk(directory):
+ for folder_name, _, filenames in os.walk(directory):
saved_scripts += filter(None, [_get_script_for_folder(folder_name, filenames)])
return saved_scripts
diff --git a/tilia/ui/cli/timelines/list.py b/tilia/ui/cli/timelines/list.py
index dc8d7ba8c..1f140e417 100644
--- a/tilia/ui/cli/timelines/list.py
+++ b/tilia/ui/cli/timelines/list.py
@@ -4,7 +4,7 @@
def pprint_tlkind(kind: TlKind) -> str:
- return kind.value.strip("_TIMELINE").capitalize()
+ return kind.value.replace("_TIMELINE", "").capitalize()
def setup_parser(subparser):
diff --git a/tilia/ui/cli/ui.py b/tilia/ui/cli/ui.py
index e4bcd6762..b5c33d358 100644
--- a/tilia/ui/cli/ui.py
+++ b/tilia/ui/cli/ui.py
@@ -111,7 +111,7 @@ def parse_and_run(self, cmd):
def run(self, cmd: str) -> bool:
"""
Parses the commands entered by the user.
- Return True if an uncaught exception ocurred.
+ Return True if an uncaught exception occurred.
The exception is stored in self.exception.
"""
try:
@@ -150,6 +150,10 @@ def get_player_class(media_type: str):
def show_crash_dialog(exc_message) -> None:
post(Post.DISPLAY_ERROR, "CLI has crashed", "Error: " + exc_message)
+ @staticmethod
+ def exit(code: int):
+ raise SystemExit(code)
+
def on_ask_yes_or_no(title: str, prompt: str) -> bool:
return ask_yes_or_no(f"{title}: {prompt}")
diff --git a/tilia/ui/qtui.py b/tilia/ui/qtui.py
index e8248cab4..bd610d7f1 100644
--- a/tilia/ui/qtui.py
+++ b/tilia/ui/qtui.py
@@ -313,7 +313,7 @@ def launch(self):
return self.q_application.exec()
def exit(self, code: int):
- # Code = 0 means a succesful run, code = 1 means an unhandled exception.
+ # Code = 0 means a successful run, code = 1 means an unhandled exception.
self.q_application.exit(code)
def get_window_geometry(self):
@@ -416,7 +416,7 @@ def on_media_load_youtube():
def on_clear_ui(self):
"""Closes all UI windows."""
- for kind, window in self._windows.items():
+ for window in self._windows.values():
if window is not None:
window.close()
self.main_window.setFocus()
diff --git a/tilia/ui/timelines/base/element.py b/tilia/ui/timelines/base/element.py
index 622ec4fcc..4cb4b5a34 100644
--- a/tilia/ui/timelines/base/element.py
+++ b/tilia/ui/timelines/base/element.py
@@ -64,7 +64,7 @@ def is_selected(self):
return self in self.timeline_ui.selected_elements
@abstractmethod
- def child_items(self):
+ def child_items(self) -> list[Any]:
...
def selection_triggers(self):
@@ -86,12 +86,18 @@ def on_right_click(self, x, y, _):
menu = self.CONTEXT_MENU_CLASS(self)
menu.exec(QPoint(x, y))
+ @abstractmethod
def on_select(self):
...
+ @abstractmethod
def on_deselect(self):
...
+ @abstractmethod
+ def update_position(self):
+ ...
+
def delete(self):
for item in self.child_items():
if item.parentItem():
diff --git a/tilia/ui/timelines/base/element_manager.py b/tilia/ui/timelines/base/element_manager.py
index cb4360c8f..4a331b27e 100644
--- a/tilia/ui/timelines/base/element_manager.py
+++ b/tilia/ui/timelines/base/element_manager.py
@@ -70,10 +70,10 @@ def _remove_from_elements_set(self, element: TE) -> None:
try:
self._elements.remove(element)
del self.id_to_element[element.id]
- except ValueError:
+ except ValueError as e:
raise ValueError(
f"Can't remove element '{element}' from {self}: not in self._elements."
- )
+ ) from e
def get_element(self, id: int) -> TE:
return self.id_to_element[id]
@@ -173,11 +173,11 @@ def _add_to_selected_elements_set(self, element: TE) -> None:
def _remove_from_selected_elements_set(self, element: TE) -> None:
try:
self._selected_elements.remove(element)
- except ValueError:
+ except ValueError as e:
raise ValueError(
f"Can't remove element '{element}' from selected objects of {self}: not"
" in self._selected_elements."
- )
+ ) from e
def get_selected_elements(self) -> list[TE]:
return self._selected_elements
diff --git a/tilia/ui/timelines/base/timeline.py b/tilia/ui/timelines/base/timeline.py
index 104a8788c..8d011197f 100644
--- a/tilia/ui/timelines/base/timeline.py
+++ b/tilia/ui/timelines/base/timeline.py
@@ -538,7 +538,7 @@ def delete_element(self, element: T):
self.element_manager.delete_element(element)
def validate_copy(self, elements: list[T]) -> None:
- """Can be overwritten by subclsses"""
+ """Can be overwritten by subclasses"""
def validate_paste(
self, paste_data: dict, elements_to_receive_paste: list[T]
diff --git a/tilia/ui/timelines/beat/element.py b/tilia/ui/timelines/beat/element.py
index 41a430862..164d93213 100644
--- a/tilia/ui/timelines/beat/element.py
+++ b/tilia/ui/timelines/beat/element.py
@@ -115,7 +115,7 @@ def seek_time(self):
return self.time
def child_items(self):
- return self.body, self.label
+ return [self.body, self.label]
def update_time(self):
self.update_position()
@@ -133,16 +133,16 @@ def update_label(self):
self.label.set_position(self.x, self.label_y)
def selection_triggers(self):
- return self.body, self.label
+ return [self.body, self.label]
def left_click_triggers(self):
- return (self.body,)
+ return [self.body]
def on_left_click(self, _) -> None:
self.setup_drag()
def double_left_click_triggers(self):
- return self.body, self.label
+ return [self.body, self.label]
def on_double_left_click(self, _) -> None:
if self.drag_manager:
@@ -150,9 +150,8 @@ def on_double_left_click(self, _) -> None:
self.drag_manager = None
post(Post.PLAYER_SEEK, self.seek_time)
- @property
def right_click_triggers(self):
- return self.body, self.label
+ return [self.body, self.label]
def setup_drag(self):
self.drag_manager = DragManager(
@@ -266,7 +265,7 @@ def set_position(self, x, y):
def set_text(self, value: str):
if not value:
- # Settting plain text to empty string
+ # Setting plain text to empty string
# keeps the graphics item interactable,
# so we need to hide it, instead.
self.setVisible(False)
diff --git a/tilia/ui/timelines/beat/timeline.py b/tilia/ui/timelines/beat/timeline.py
index 78597faab..c895e3186 100644
--- a/tilia/ui/timelines/beat/timeline.py
+++ b/tilia/ui/timelines/beat/timeline.py
@@ -275,7 +275,7 @@ def update_measure_numbers(self):
# State is being restored and
# beats in measure has not been
# updated yet. This is a dangerous
- # workaroung, as it might conceal
+ # workaround, as it might conceal
# other exceptions. Let's fix this ASAP.
continue
diff --git a/tilia/ui/timelines/collection/collection.py b/tilia/ui/timelines/collection/collection.py
index c454c71f9..e49acdf5e 100644
--- a/tilia/ui/timelines/collection/collection.py
+++ b/tilia/ui/timelines/collection/collection.py
@@ -569,11 +569,11 @@ def _add_to_timeline_uis_set(self, timeline_ui: TimelineUI) -> None:
def _remove_from_timeline_uis_set(self, timeline_ui: TimelineUI) -> None:
try:
self._timeline_uis.remove(timeline_ui)
- except ValueError:
+ except ValueError as e:
raise ValueError(
f"Can't remove timeline ui '{timeline_ui}' from {self}: not in"
" self.timeline_uis."
- )
+ ) from e
def _add_to_timeline_ui_select_order(self, tl_ui: TimelineUI) -> None:
self._select_order.insert(0, tl_ui)
@@ -581,18 +581,18 @@ def _add_to_timeline_ui_select_order(self, tl_ui: TimelineUI) -> None:
def _remove_from_timeline_ui_select_order(self, tl_ui: TimelineUI) -> None:
try:
self._select_order.remove(tl_ui)
- except ValueError:
+ except ValueError as e:
raise ValueError(
f"Can't remove timeline ui '{tl_ui}' from select order: not in select"
" order."
- )
+ ) from e
def _send_to_top_of_select_order(self, tl_ui: TimelineUI):
self._select_order.remove(tl_ui)
self._select_order.insert(0, tl_ui)
def add_timeline_view_to_scene(self, view: QGraphicsView, ordinal: int) -> None:
- view.proxy = self.scene.addWidget(view)
+ self.scene.addWidget(view)
y = sum(tlui.get_data("height") for tlui in sorted(self)[: ordinal - 1])
view.move(0, y)
self.update_height()
@@ -1121,8 +1121,8 @@ def filter_for_pasting(_) -> list[TimelineUI]:
try:
return selector_to_func[selector](get_by_kinds(kinds))
- except KeyError:
- raise NotImplementedError(f"Can't select with {selector=}")
+ except KeyError as e:
+ raise NotImplementedError(f"Can't select with {selector=}") from e
@command_callback
def on_timeline_ordinal_permute(self, tlui1: TimelineUI, tlui2: TimelineUI):
diff --git a/tilia/ui/timelines/copy_paste.py b/tilia/ui/timelines/copy_paste.py
index 5dcc8ac98..a25886d2d 100644
--- a/tilia/ui/timelines/copy_paste.py
+++ b/tilia/ui/timelines/copy_paste.py
@@ -11,7 +11,7 @@ def get_copy_data_from_elements(
elements: list[tuple[TimelineUIElement, CopyAttributes]],
) -> list[dict]:
copy_data = []
- for element, kind, copy_attrs in elements:
+ for element, copy_attrs in elements:
copy_data.append(get_copy_data_from_element(element, copy_attrs))
return copy_data
diff --git a/tilia/ui/timelines/hierarchy/element.py b/tilia/ui/timelines/hierarchy/element.py
index 1948b98c6..20f851e2e 100644
--- a/tilia/ui/timelines/hierarchy/element.py
+++ b/tilia/ui/timelines/hierarchy/element.py
@@ -76,7 +76,7 @@ class HierarchyUI(TimelineUIElement):
support_by_component_value=["start", "pre_start", "end", "level"],
)
- NAME_WHEN_UNLABELED = "Unnamed"
+ NAME_WHEN_UNLABELLED = "Unnamed"
FULL_NAME_SEPARATOR = "-"
UPDATE_TRIGGERS = [
@@ -188,12 +188,12 @@ def get_cropped_label(self, start_x, end_x, label):
@property
def full_name(self) -> str:
- partial_name = self.get_data("label") or self.NAME_WHEN_UNLABELED
+ partial_name = self.get_data("label") or self.NAME_WHEN_UNLABELLED
next_parent = self.get_data("parent")
while next_parent:
- parent_name = next_parent.get_data("label") or self.NAME_WHEN_UNLABELED
+ parent_name = next_parent.get_data("label") or self.NAME_WHEN_UNLABELLED
partial_name = parent_name + self.FULL_NAME_SEPARATOR + partial_name
next_parent = next_parent.parent
@@ -454,8 +454,8 @@ def extremity_to_handle(
HierarchyUI.Extremity.PRE_START: self.pre_start_handle,
HierarchyUI.Extremity.POST_END: self.post_end_handle,
}[extremity]
- except KeyError:
- raise ValueError("Unrecognized extremity")
+ except KeyError as e:
+ raise ValueError("Unrecognized extremity") from e
@staticmethod
def frame_to_body_extremity(
@@ -468,8 +468,8 @@ def frame_to_body_extremity(
HierarchyUI.Extremity.PRE_START: HierarchyUI.Extremity.START,
HierarchyUI.Extremity.POST_END: HierarchyUI.Extremity.END,
}[extremity]
- except KeyError:
- raise ValueError("Unrecognized extremity")
+ except KeyError as e:
+ raise ValueError("Unrecognized extremity") from e
def handle_to_extremity(self, handle: HierarchyBodyHandle | HierarchyFrameHandle):
try:
@@ -479,8 +479,8 @@ def handle_to_extremity(self, handle: HierarchyBodyHandle | HierarchyFrameHandle
self.pre_start_handle: HierarchyUI.Extremity.PRE_START,
self.post_end_handle: HierarchyUI.Extremity.POST_END,
}[handle]
- except KeyError:
- raise ValueError(f"{handle} if not a handle of {self}")
+ except KeyError as e:
+ raise ValueError(f"{handle} if not a handle of {self}") from e
@staticmethod
def extremity_to_x(extremity: HierarchyUI.Extremity, start_x, end_x):
@@ -501,7 +501,7 @@ def _setup_handle(self, extremity: HierarchyUI.Extremity):
)
def selection_triggers(self):
- return self.body, self.label, self.comments_icon
+ return [self.body, self.label, self.comments_icon]
def left_click_triggers(self):
triggers = [
@@ -531,7 +531,7 @@ def on_double_left_click(self, _) -> None:
post(Post.PLAYER_SEEK, self.seek_time)
def right_click_triggers(self):
- return self.body, self.label, self.comments_icon
+ return [self.body, self.label, self.comments_icon]
def on_select(self) -> None:
self.body.on_select()
@@ -554,7 +554,7 @@ def on_deselect(self) -> None:
def selected_ascendants(self) -> list[HierarchyUI]:
"""Returns hierarchies in the same branch that
- are both selected and higher-leveled than self"""
+ are both selected and higher-levelled than self"""
uis_at_start = self.timeline_ui.get_elements_by_attr("start_x", self.start_x)
selected_uis = self.timeline_ui.selected_elements
@@ -566,7 +566,7 @@ def selected_ascendants(self) -> list[HierarchyUI]:
def selected_descendants(self) -> list[HierarchyUI]:
"""Returns hierarchies in the same branch that are both
- selected and lower-leveled than self"""
+ selected and lower-levelled than self"""
uis_at_start = self.timeline_ui.get_elements_by_attr("start_x", self.start_x)
selected_uis = self.timeline_ui.selected_elements
diff --git a/tilia/ui/timelines/pdf/timeline.py b/tilia/ui/timelines/pdf/timeline.py
index 5f83a47b8..d9245cf0e 100644
--- a/tilia/ui/timelines/pdf/timeline.py
+++ b/tilia/ui/timelines/pdf/timeline.py
@@ -99,7 +99,7 @@ def page_total(self):
# prevents it from capping the
# number at a value that might be lower than
# the page total in a PDF loaded in the future.
- # Can't use math.inf because PyQt requires an int.
+ # Can't use math.inf because pyside requires an int.
return 99999999
return self.timeline.get_data("page_total")
diff --git a/tilia/ui/timelines/score/element/barline.py b/tilia/ui/timelines/score/element/barline.py
index f62cb3d57..f277087ee 100644
--- a/tilia/ui/timelines/score/element/barline.py
+++ b/tilia/ui/timelines/score/element/barline.py
@@ -12,6 +12,7 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.body = None
+ @property
def x(self):
return time_x_converter.get_x_by_time(self.get_data("time"))
@@ -23,7 +24,7 @@ def child_items(self):
def get_body_args(self):
return (
- self.x(),
+ self.x,
self.timeline_ui.staff_y_cache.values(),
)
@@ -41,6 +42,12 @@ def on_components_deserialized(self):
def selection_triggers(self):
return []
+ def on_deselect(self):
+ return
+
+ def on_select(self):
+ return
+
class BarLineBody:
def __init__(self, x, ys: list[tuple[float, float]]):
@@ -56,5 +63,5 @@ def _setup_lines(self, x: float, ys: list[tuple[float, float]]):
self.set_position(x, ys)
def set_position(self, x: float, ys: list[tuple[float, float]]):
- for line, (y0, y1) in zip(self.lines, ys):
+ for line, (y0, y1) in zip(self.lines, ys, strict=True):
line.setLine(x, y0, x, y1)
diff --git a/tilia/ui/timelines/score/element/clef.py b/tilia/ui/timelines/score/element/clef.py
index cf5814ee3..f671df1f8 100644
--- a/tilia/ui/timelines/score/element/clef.py
+++ b/tilia/ui/timelines/score/element/clef.py
@@ -61,6 +61,12 @@ def selection_triggers(self):
def shorthand(self) -> Clef.Shorthand | None:
return self.tl_component.shorthand()
+ def on_deselect(self):
+ return
+
+ def on_select(self):
+ return
+
class ClefBody(QGraphicsPixmapItem):
def __init__(self, x: float, y: float, height: float, path: Path):
diff --git a/tilia/ui/timelines/score/element/key_signature.py b/tilia/ui/timelines/score/element/key_signature.py
index baa64b080..ff6520d6a 100644
--- a/tilia/ui/timelines/score/element/key_signature.py
+++ b/tilia/ui/timelines/score/element/key_signature.py
@@ -19,7 +19,7 @@ def __init__(self, *args, **kwargs):
@staticmethod
def _clef_shorthand_to_icon_path_string(shorthand: Clef.Shorthand | None) -> str:
if not shorthand:
- # Key signature not implmemented
+ # Key signature not implemented
# for custom clefs. Using "treble"
# just to prevent a crash
return "treble"
@@ -84,6 +84,12 @@ def on_components_deserialized(self):
self.scene.removeItem(self.body)
self._setup_body()
+ def on_deselect(self):
+ return
+
+ def on_select(self):
+ return
+
class KeySignatureBody(QGraphicsPixmapItem):
def __init__(self, x: float, y: float, height: int, path: str):
diff --git a/tilia/ui/timelines/score/element/note/ui.py b/tilia/ui/timelines/score/element/note/ui.py
index f09e31d09..684dc21cc 100644
--- a/tilia/ui/timelines/score/element/note/ui.py
+++ b/tilia/ui/timelines/score/element/note/ui.py
@@ -242,20 +242,20 @@ def get_accidental_height(accidental: int, scale_factor: float) -> int:
def get_accidental_scale_factor(self):
"""
Scales accidental according to amw = average measure width.
- If amw < visibility_treshold, returns 0, indicating accidentals should be hidden.
- If visibility_treshold < amw < max_size_treshold, scales proportionally with min_scale as a minimum.
- If amw > max_size_treshold, returns 1, indicating accidentals should be fully visible.
+ If amw < visibility_threshold, returns 0, indicating accidentals should be hidden.
+ If visibility_threshold < amw < max_size_threshold, scales proportionally with min_scale as a minimum.
+ If amw > max_size_threshold, returns 1, indicating accidentals should be fully visible.
"""
- visibility_treshold = 30
- max_size_treshold = 180
+ visibility_threshold = 30
+ max_size_threshold = 180
min_scale = 0.5
average_measure_width = self.timeline_ui.average_measure_width()
if not average_measure_width:
return 1
- if average_measure_width < visibility_treshold:
+ if average_measure_width < visibility_threshold:
return 0
return min(
- 1, min_scale + (average_measure_width / max_size_treshold * min_scale)
+ 1, min_scale + (average_measure_width / max_size_threshold * min_scale)
)
def update_color(self):
diff --git a/tilia/ui/timelines/score/element/staff.py b/tilia/ui/timelines/score/element/staff.py
index 87007a054..05d31bcf5 100644
--- a/tilia/ui/timelines/score/element/staff.py
+++ b/tilia/ui/timelines/score/element/staff.py
@@ -51,6 +51,12 @@ def child_items(self):
def selection_triggers(self):
return []
+ def on_deselect(self):
+ return
+
+ def on_select(self):
+ return
+
class StaffLines:
COLOR = "gray"
diff --git a/tilia/ui/timelines/score/element/time_signature.py b/tilia/ui/timelines/score/element/time_signature.py
index f31f7cfcc..0443f6fa5 100644
--- a/tilia/ui/timelines/score/element/time_signature.py
+++ b/tilia/ui/timelines/score/element/time_signature.py
@@ -68,6 +68,12 @@ def on_components_deserialized(self):
def selection_triggers(self):
return []
+ def on_deselect(self):
+ return
+
+ def on_select(self):
+ return
+
class TimeSignatureBody(QGraphicsItem):
def __init__(
@@ -96,7 +102,7 @@ def get_scaled_pixmap(self, digit: int | str, height: int):
def set_numerator_items(self, numerator: int, height: int):
self.numerator_items = []
for i, digit in enumerate(str(numerator)):
- item = QGraphicsPixmapItem(self.get_scaled_pixmap(digit, height), self)
+ item = NumberPixmap(self.get_scaled_pixmap(digit, height), self)
item.digit = int(digit)
item.setPos(i * item.pixmap().width(), 0)
self.numerator_items.append(item)
@@ -104,7 +110,7 @@ def set_numerator_items(self, numerator: int, height: int):
def set_denominator_items(self, denominator: int, height: int):
self.denominator_items = []
for i, digit in enumerate(str(denominator)):
- item = QGraphicsPixmapItem(self.get_scaled_pixmap(digit, height), self)
+ item = NumberPixmap(self.get_scaled_pixmap(digit, height), self)
item.digit = int(digit)
item.setPos(i * item.pixmap().width(), item.pixmap().height())
self.denominator_items.append(item)
@@ -145,5 +151,6 @@ def boundingRect(self):
bounding_rect = bounding_rect.united(item.boundingRect())
return bounding_rect
- def paint(self, painter, option, widget):
- ...
+
+class NumberPixmap(QGraphicsPixmapItem):
+ digit = 0
diff --git a/tilia/ui/timelines/score/timeline.py b/tilia/ui/timelines/score/timeline.py
index b7739f1b9..301fc2943 100644
--- a/tilia/ui/timelines/score/timeline.py
+++ b/tilia/ui/timelines/score/timeline.py
@@ -448,8 +448,8 @@ def _validate_staff_numbers(self) -> bool:
def average_measure_width(self) -> float:
if self._measure_count == 0:
return 0
- x0 = self.first_bar_line.x()
- x1 = self.last_bar_line.x()
+ x0 = self.first_bar_line.x
+ x1 = self.last_bar_line.x
return (x1 - x0) / self._measure_count
def on_audio_time_change(self, time: float, _) -> None:
diff --git a/tilia/ui/windows/settings.py b/tilia/ui/windows/settings.py
index b6e648e3c..afb0d81e9 100644
--- a/tilia/ui/windows/settings.py
+++ b/tilia/ui/windows/settings.py
@@ -195,7 +195,7 @@ def pretty_label(input_string: str):
)
-def select_color_button(value, text=None):
+def select_color_button(value, text=""):
def select_color(old_color):
new_color = QColorDialog.getColor(
QColor(old_color),
@@ -212,7 +212,7 @@ def set_color(color):
)
def get_value():
- return button.styleSheet().lstrip("background-color: ").split(";")[0]
+ return button.styleSheet().replace("background-color: ", "").split(";")[0]
button = QPushButton()
button.setText(text)
@@ -231,7 +231,7 @@ def combobox(options: list, current_value: str):
return combobox
-def get_widget_for_value(value, text=None) -> QWidget:
+def get_widget_for_value(value, text="") -> QWidget:
match value:
case bool():
checkbox = QCheckBox()
@@ -284,7 +284,15 @@ def get_widget_for_value(value, text=None) -> QWidget:
raise NotImplementedError
-def get_value_for_widget(widget: QWidget):
+def get_value_for_widget(
+ widget: QSpinBox
+ | QTextEdit
+ | QWidget
+ | QCheckBox
+ | QPushButton
+ | QComboBox
+ | QLineEdit,
+):
match widget.objectName():
case "int":
return widget.value()
@@ -304,7 +312,7 @@ def get_value_for_widget(widget: QWidget):
return widget.isChecked()
case "color":
- return widget.styleSheet().lstrip("background-color: ").split(";")[0]
+ return widget.styleSheet().replace("background-color: ", "").split(";")[0]
case "combobox":
text = widget.currentText().lower()
From a6d1b1aeef57c218f3c977f74ddf1135b6888fd4 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Thu, 12 Feb 2026 15:17:50 +0000
Subject: [PATCH 36/75] ci: don't test python 3.13 on windows
no support
---
.github/workflows/run-tests.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index ad64908e2..40ee4a07a 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -33,6 +33,9 @@ jobs:
path: ~/Library/Caches/pip
- os: windows-latest
path: ~\AppData\Local\pip\Cache
+ exclude:
+ - os: windows-latest
+ python-version: '3.13' # no pyside support
timeout-minutes: 30
env:
QT_SELECT: "qt6"
From 2d57cdd1c3f75f48c8e040654c2f459f0df42949 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Thu, 12 Feb 2026 15:18:30 +0000
Subject: [PATCH 37/75] chore: update docs
---
README.md | 28 ++++++++++++----------------
TESTING.md | 2 ++
2 files changed, 14 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index 0c54e1489..774e7c663 100644
--- a/README.md
+++ b/README.md
@@ -62,10 +62,9 @@ Before you start, you will need:
| `git` | Download [here](https://git-scm.com/install) | Not necessary for a direct download from this repository |
-### Note for Linux users
+### Note to Linux users
Users have reported dependency issues when running TiLiA on Linux (see [#370](https://github.com/TimeLineAnnotator/desktop/issues/370) and [#371](https://github.com/TimeLineAnnotator/desktop/issues/371)).
-That is probably due to `PyInstaller` not being completely compatible with `PyQt`.
-We are working to fix that with with a new build process using `pyside6-deploy` instead.
+Due to system variations between Linux distributions, some additional system dependencies may be required. Visit our [help page](https://tilia-app.com/help/installation#troubleshooting-linux) for more information.
### Running from source
@@ -81,17 +80,12 @@ Change directory to the cloned repository:
cd tilia-desktop
```
Note: We recommend using a clean [virtual environment](https://docs.python.org/3/library/venv.html) for the next steps.
-Failure to do so is likely to cause issues with dependencies.
+Failure to do so may cause issues with dependencies.
Install TiLiA and its dependencies with:
```
pip install -e .
```
-On Linux, some additional Qt dependencies are required:
-```
-sudo apt install libnss3 libasound libxkbfile1 libpulse0
-```
-| n.b.: the specific names of these packages varies based on your Linux distribution.
To run TiLiA from source, run:
```
@@ -104,18 +98,20 @@ python -m tilia --user-interface cli
```
### Building from source
-TiLiA uses [PyInstaller](https://pyinstaller.org/en/stable/) to build binaries.
-Note that the binaries will be for the platform you are building on, as `PyInstaller` supports no cross-compilation.
+TiLiA uses [Nuitka](https://nuitka.net/) to build binaries.
+Note that the binaries will be for the platform you are building on, as `Nuitka` supports no cross-compilation.
-After cloning tilia and installing the dependencies (see above), install `PyInstaller` with:
+After cloning TiLiA, install TiLiA's run and build dependencies with:
```
-pip install pyinstaller
+pip install -e . --group build
```
-To build a stand-alone executable, run PyInstaller with the settings in `tilia.spec`:
+To build a stand-alone executable, run the script:
```
-pyinstaller tilia.spec
+python scripts/deploy.py [ref_name] [os_type]
```
-The executable will be created in `dist` folder inside the project directory.
+(*n.b.: `ref_name` and `os_type` are arbitrary strings that do not affect the build outcome.)
+
+The executable will be found in the `build/[os_type]/exe` folder in the project directory.
## Planned features
diff --git a/TESTING.md b/TESTING.md
index 8593df908..b371ef7f5 100644
--- a/TESTING.md
+++ b/TESTING.md
@@ -1,4 +1,6 @@
The test suite is written in pytest. Below are some things to keep in my mind when writing tests. For examples of good and thorough tests, see `tests\ui\timelines\test_marker_timeline_ui.py`. Older modules should be refactored at some point to follow the guidelines below.
+## Pre-requisites
+`pip install --group testing`
## How to simulate interaction with the UI?
- The `user_actions` fixture can be used to trigger actions on the UI. This is equivalent to pressing buttons on the UI. We should also check that the actions are available in the UI where we expect them.
- The `tilia_state` fixture can be used to make certain changes to state simulating user input (e.g. `tilia_state.current_time = 10`).
From f7b0c91ca5e97d7986ae843de775cda17b325bc8 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Thu, 12 Feb 2026 15:54:23 +0000
Subject: [PATCH 38/75] fix: locating start of script
---
pyproject.toml | 2 +-
tilia/__main__.py | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 87519f383..0e7b5d204 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,7 +36,7 @@ name = "Felipe Defensor et al."
email = "tilia@tilia-app.com"
[project.gui-scripts]
-tilia-desktop = "tilia.__main__:boot"
+tilia-desktop = "tilia.__main__:main"
[project.urls]
Homepage = "https://tilia-app.com"
diff --git a/tilia/__main__.py b/tilia/__main__.py
index 544386887..9b6c5dba8 100644
--- a/tilia/__main__.py
+++ b/tilia/__main__.py
@@ -46,7 +46,7 @@ def deps_debug(exc: ImportError):
raise RuntimeError("Install the necessary dependencies then restart.") from exc
-if __name__ == "__main__":
+def main():
try:
from tilia.boot import boot # noqa: E402
@@ -55,3 +55,7 @@ def deps_debug(exc: ImportError):
if sys.platform != "linux":
raise exc
deps_debug(exc)
+
+
+if __name__ == "__main__":
+ main()
From 1dcdd73fed49a325a88ca1513ec5308dc44ce6a9 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 10:56:32 +0000
Subject: [PATCH 39/75] fix: typing
---
tilia/ui/timelines/collection/collection.py | 17 ++++++++++-------
tilia/ui/timelines/view.py | 10 ++++++++--
2 files changed, 18 insertions(+), 9 deletions(-)
diff --git a/tilia/ui/timelines/collection/collection.py b/tilia/ui/timelines/collection/collection.py
index e49acdf5e..28be3610d 100644
--- a/tilia/ui/timelines/collection/collection.py
+++ b/tilia/ui/timelines/collection/collection.py
@@ -7,7 +7,6 @@
from PySide6.QtCore import Qt, QPoint
from PySide6.QtWidgets import (
- QGraphicsView,
QMainWindow,
QGraphicsItem,
QGraphicsScene,
@@ -42,6 +41,8 @@
from tilia.ui.timelines.view import TimelineView
from .view import TimelineUIsView
from ..base.element import TimelineUIElement
+from ..beat import BeatTimelineUI
+from ..harmony import HarmonyTimelineUI
from ..selection_box import SelectionBoxQt
from ..slider.timeline import SliderTimelineUI
from ...dialogs.add_timeline_without_media import AddTimelineWithoutMedia
@@ -74,7 +75,7 @@ def __init__(
kind: None for kind in TimelineKind if kind != TlKind.SLIDER_TIMELINE
}
- self._timeline_uis = set()
+ self._timeline_uis: set[TimelineUI] = set()
self._select_order = []
self._timeline_uis_to_playback_line_ids = {}
self.sb_items_to_selected_items = {}
@@ -591,7 +592,8 @@ def _send_to_top_of_select_order(self, tl_ui: TimelineUI):
self._select_order.remove(tl_ui)
self._select_order.insert(0, tl_ui)
- def add_timeline_view_to_scene(self, view: QGraphicsView, ordinal: int) -> None:
+ def add_timeline_view_to_scene(self, view: TimelineView, ordinal: int) -> None:
+ view.proxy = self.scene.addWidget(view)
self.scene.addWidget(view)
y = sum(tlui.get_data("height") for tlui in sorted(self)[: ordinal - 1])
view.move(0, y)
@@ -646,7 +648,7 @@ def update_timeline_ui_ordinal(self):
@staticmethod
def update_timeline_times(tlui: TimelineUI):
if tlui.TIMELINE_KIND == TlKind.SLIDER_TIMELINE:
- tlui: SliderTimelineUI
+ tlui = cast(SliderTimelineUI, tlui)
tlui.update_items_position()
else:
tlui.element_manager.update_time_on_elements()
@@ -693,7 +695,7 @@ def _get_timeline_ui_by_view(self, view):
def _on_timeline_ui_right_click(
self,
- view: QGraphicsView,
+ view: TimelineView,
x: int,
y: int,
item: Optional[QGraphicsItem],
@@ -718,7 +720,7 @@ def _on_timeline_ui_right_click(
def _on_timeline_ui_left_click(
self,
- view: QGraphicsView,
+ view: TimelineView,
x: int,
y: int,
item: Optional[QGraphicsItem],
@@ -915,7 +917,8 @@ def on_hierarchy_merge_split(self, new_units: list, old_units: list):
self._update_loop_elements()
def on_harmony_timeline_components_deserialized(self, id):
- self.get_timeline_ui(id).on_timeline_components_deserialized() # noqa
+ timeline_ui = cast(HarmonyTimelineUI, self.get_timeline_ui(id))
+ timeline_ui.on_timeline_components_deserialized()
def on_beat_timeline_components_deserialized(self, id: int):
from tilia.ui.timelines.beat import BeatTimelineUI
diff --git a/tilia/ui/timelines/view.py b/tilia/ui/timelines/view.py
index 5fe292fc9..a0393b0e6 100644
--- a/tilia/ui/timelines/view.py
+++ b/tilia/ui/timelines/view.py
@@ -8,7 +8,13 @@
QColor,
QBrush,
)
-from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QSizePolicy, QFrame
+from PySide6.QtWidgets import (
+ QFrame,
+ QGraphicsProxyWidget,
+ QGraphicsScene,
+ QGraphicsView,
+ QSizePolicy,
+)
from tilia.settings import settings
from tilia.requests import post, Post, Get, get, listen
@@ -39,7 +45,7 @@ def __init__(self, scene: QGraphicsScene):
)
self.dragging = False
- self.proxy = None # will be set by TimelineUIs
+ self.proxy = QGraphicsProxyWidget() # will be set by TimelineUIs
def on_settings_updated(self, updated_settings):
if "general" in updated_settings:
From 4e5f874be84dcce6f6568e8da2cc8a11e8621bc0 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 10:58:28 +0000
Subject: [PATCH 40/75] fix: stdout not showing
this is apparently a windows specific difference
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 0e7b5d204..cbdddfcd0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -35,7 +35,7 @@ dependencies = [
name = "Felipe Defensor et al."
email = "tilia@tilia-app.com"
-[project.gui-scripts]
+[project.scripts]
tilia-desktop = "tilia.__main__:main"
[project.urls]
From ec00b76f8793791a03ffdf2b09c3dd8aa93daa32 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 10:59:40 +0000
Subject: [PATCH 41/75] chore: shorten name
---
README.md | 4 ++--
pyproject.toml | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 774e7c663..3570e4aa4 100644
--- a/README.md
+++ b/README.md
@@ -89,12 +89,12 @@ pip install -e .
To run TiLiA from source, run:
```
-tilia-desktop
+tilia
```
TiLiA also offers a CLI mode, which can be run with:
```
-python -m tilia --user-interface cli
+tilia --user-interface cli
```
### Building from source
diff --git a/pyproject.toml b/pyproject.toml
index cbdddfcd0..19346394b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,7 +36,7 @@ name = "Felipe Defensor et al."
email = "tilia@tilia-app.com"
[project.scripts]
-tilia-desktop = "tilia.__main__:main"
+tilia = "tilia.__main__:main"
[project.urls]
Homepage = "https://tilia-app.com"
From d06509ddb24f50600e893d62820941e016445d9c Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 11:02:19 +0000
Subject: [PATCH 42/75] chore: bump year
---
tilia/constants.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tilia/constants.py b/tilia/constants.py
index 6468db56d..6324dfc98 100644
--- a/tilia/constants.py
+++ b/tilia/constants.py
@@ -30,7 +30,7 @@
APP_NAME = setupcfg.get("name", "")
VERSION = setupcfg.get("version", "0.0.0")
-YEAR = "2022-2025"
+YEAR = "2022-2026"
FILE_EXTENSION = "tla"
EMAIL_URL = "mailto:" + EMAIL
From 6f65b2b290fc05db4be3ffc686f09dbf33a0353e Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 11:16:58 +0000
Subject: [PATCH 43/75] docs: update
and fix some typing...
---
tilia/media/player/qtplayer.py | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/tilia/media/player/qtplayer.py b/tilia/media/player/qtplayer.py
index 2ddef4eb9..c158af543 100644
--- a/tilia/media/player/qtplayer.py
+++ b/tilia/media/player/qtplayer.py
@@ -16,19 +16,28 @@
def wait_for_signal(signal: SignalInstance, value):
+ """
+ Many Qt functions run on threads, and this wrapper makes sure that after starting a process, the right signal is emitted before continuing the TiLiA process.
+ See _engine_stop of QtPlayer for an example implementation.
+
+ :param signal: The signal to watch.
+ :type signal: SignalInstance
+ :param value: The "right" output value that signal should emit before continuing. (eg. on stopping player, playbackStateChanged emits StoppedState when player has been successfully stopped. Only then can we continue the rest of the update process.)
+ """
+
def signal_wrapper(func):
timer = QTimer(singleShot=True, interval=100)
loop = QEventLoop()
- success = False # noqa: F841
+ success = False
def value_checker(signal_value):
if signal_value == value:
- global success
+ nonlocal success
success = True
loop.quit()
def check_signal(*args, **kwargs):
- global success
+ nonlocal success
if not func(*args, **kwargs):
return False
signal.connect(value_checker)
From 80cb886c218839e2d6fbc431c4335596795899aa Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 11:30:32 +0000
Subject: [PATCH 44/75] docs: update deploy script
---
scripts/deploy.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/scripts/deploy.py b/scripts/deploy.py
index 5af75a236..adc0d91ee 100644
--- a/scripts/deploy.py
+++ b/scripts/deploy.py
@@ -50,8 +50,11 @@ def _handle_inputs():
assert len(sys.argv) == 3, "Incorrect number of inputs"
global ref_name, build_os, outdir
ref_name = sys.argv[1]
+ # in a git action runner, sys.argv[2] could look like someOS-latest, someOS-22.02, etc, where someOS is probably macos, ubuntu or windows.
+ # we save build_os as the runner os stripped of "latest" and any digits: just someOS.
if "macos" in sys.argv[2] and "intel" not in sys.argv[2]:
build_os = "macos-silicon"
+ # to identify the difference between macos-silicon and macos-intel. currently uses the images macos-latest and macos-15-intel (which are silicon and intel respectively.)
else:
build_os = "-".join(
[
From da796bf190dfa66a1fab96fda7b4e91a7c73be0c Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 12:33:43 +0000
Subject: [PATCH 45/75] ci: more descriptive build-test
---
.github/workflows/build.yml | 23 +++++++++++++++++++++--
1 file changed, 21 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e25ac6d90..c0fec5358 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -65,7 +65,14 @@ jobs:
run: |
brew install coreutils
echo "Checking dependencies"
+ # test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is less than 10s.
+ # then zip everything up and delete the original non-zipped file.
+ start=$(date +%s)
echo "gtimeout 10 ${{ steps.build-exe.outputs.out-filepath }}/Contents/MacOS/${{ steps.build-exe.outputs.out-filename }}" | bash || true
+ if (($(date +%s) - $start < 10)); then
+ echo "::error::Process terminated early! Potential errors in build."
+ exit 1
+ fi
zip -r9 ${{ steps.build-exe.outputs.zip-filepath }} ${{ steps.build-exe.outputs.out-filepath }} || true
rm -rf ${{ steps.build-exe.outputs.out-filepath }}
@@ -74,7 +81,13 @@ jobs:
shell: bash
run: |
echo "Checking dependencies"
- echo "timeout 30 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true
+ # test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is not 10s.
+ start=$(date +%s)
+ echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true
+ if (($(date +%s) - $start < 10)); then
+ echo "::error::Process terminated early! Potential errors in build."
+ exit 1
+ fi
- name: Test executable [Linux]
id: set-path
@@ -83,7 +96,13 @@ jobs:
run: |
echo "path=${{ steps.build-exe.outputs.out-filename }}" >> "$GITHUB_OUTPUT"
echo "Checking dependencies"
- echo "timeout 30 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true
+ # test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is not 10s.
+ start=$(date +%s)
+ echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true
+ if (($(date +%s) - $start < 10)); then
+ echo "::error::Process terminated early! Potential errors in build."
+ exit 1
+ fi
- name: Upload executable
uses: actions/upload-artifact@v4
From 7fc79e8efbbec67a35ca94568a4026a20df068ff Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 14:00:38 +0000
Subject: [PATCH 46/75] fix: accidental duplicate
---
tilia/ui/timelines/collection/collection.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/tilia/ui/timelines/collection/collection.py b/tilia/ui/timelines/collection/collection.py
index 28be3610d..05ae297f4 100644
--- a/tilia/ui/timelines/collection/collection.py
+++ b/tilia/ui/timelines/collection/collection.py
@@ -594,7 +594,6 @@ def _send_to_top_of_select_order(self, tl_ui: TimelineUI):
def add_timeline_view_to_scene(self, view: TimelineView, ordinal: int) -> None:
view.proxy = self.scene.addWidget(view)
- self.scene.addWidget(view)
y = sum(tlui.get_data("height") for tlui in sorted(self)[: ordinal - 1])
view.move(0, y)
self.update_height()
From f7e3fd54251cf5788b182e212e6e42117b7d7605 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 14:13:11 +0000
Subject: [PATCH 47/75] fix: reverts 66e88dc
---
tilia/dirs.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/tilia/dirs.py b/tilia/dirs.py
index e2956efd0..762002b93 100644
--- a/tilia/dirs.py
+++ b/tilia/dirs.py
@@ -37,6 +37,10 @@ def setup_logs_path(data_dir):
def setup_dirs() -> None:
+ # if not in prod, set directory to root of tilia
+ if os.environ.get("ENVIRONMENT") != "prod":
+ os.chdir(os.path.dirname(__file__))
+
data_dir = setup_data_dir()
global autosaves_path, logs_path
From a1d390f7357c437ca5f12820d632f74e1ea437f6 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 14:19:48 +0000
Subject: [PATCH 48/75] docs: some help on nuitka-package.config
---
tilia.nuitka-package.config.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/tilia.nuitka-package.config.yml b/tilia.nuitka-package.config.yml
index b4042cbe0..e4c7429ad 100644
--- a/tilia.nuitka-package.config.yml
+++ b/tilia.nuitka-package.config.yml
@@ -1,10 +1,11 @@
+# see https://nuitka.net/user-documentation/nuitka-package-config.html for help on this file.
# yamllint disable rule:line-length
# yamllint disable rule:indentation
# yamllint disable rule:comments-indentation
# yamllint disable rule:comments
# too many spelling things, spell-checker: disable
---
-- module-name: "music21"
+- module-name: "music21" # mostly regex matching entire refrences to tests and deleting.
anti-bloat:
- description: "remove tests"
replacements_plain:
From 21e8617e4422496d5942ae7ce9841b5d739f4fe7 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 14:35:52 +0000
Subject: [PATCH 49/75] docs: script/deploy
---
scripts/deploy.py | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/scripts/deploy.py b/scripts/deploy.py
index adc0d91ee..b7ec4cc4c 100644
--- a/scripts/deploy.py
+++ b/scripts/deploy.py
@@ -1,3 +1,15 @@
+"""
+Script to build TiLiA with Nuitka.
+(a more flexible alternative to building with only pyside-deploy or Nuitka)
+pyside-deploy has very limited Nuitka-specific options and Nuitka requires a specific file structure to build.
+Hence this pyside-deploy-inspired script.
+
+- Run `python scripts/deploy.py [ref_name] [os_type]`
+- Creates sdist to create metadata file from information in pyproject and filter out unnecessary files
+- Then builds executable, which will be found in:
+ build/exe/[os_type]/TiLiA-[tilia version in pyproject](-[ref_name, if not the same as tilia version])-[os_type]
+"""
+
from colorama import Fore
import dotenv
from enum import Enum
From 29aa7332204c1a191a9f5af321d3c5ba4917a1b0 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 14:37:36 +0000
Subject: [PATCH 50/75] ci: check ci test actually works
---
tilia/boot.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/tilia/boot.py b/tilia/boot.py
index 9c70ae093..58c696b98 100644
--- a/tilia/boot.py
+++ b/tilia/boot.py
@@ -2,6 +2,7 @@
import os
import sys
import traceback
+import ImaginaryModule # noqa: F401
from PySide6.QtWidgets import QApplication
From 16b8cf5613ff2b4fad8db331a3e3397603025261 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 15:29:11 +0000
Subject: [PATCH 51/75] ci: revert b4f9444
---
tilia/boot.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/tilia/boot.py b/tilia/boot.py
index 58c696b98..9c70ae093 100644
--- a/tilia/boot.py
+++ b/tilia/boot.py
@@ -2,7 +2,6 @@
import os
import sys
import traceback
-import ImaginaryModule # noqa: F401
from PySide6.QtWidgets import QApplication
From a29f0432d7508c57a982b43db8657fbf164b1e38 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 17:10:02 +0000
Subject: [PATCH 52/75] ci: pin setuptools version
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 19346394b..991a86499 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,7 +49,7 @@ build = [
"Nuitka >= 4.0",
"ordered-set==4.1.0",
"pyyaml",
- "setuptools; python_version >= '3.12'",
+ "setuptools<82.0.0; python_version >= '3.12'",
"wheel==0.38.4",
"zstandard==0.20.0"
]
From 24c42d4f2f05d9a3f80cb255f8a521a1acb734a4 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Thu, 19 Feb 2026 11:25:45 +0000
Subject: [PATCH 53/75] fix: settings fix uninstantiated error
---
tilia/settings.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/tilia/settings.py b/tilia/settings.py
index 373dc40c4..ef597e3fb 100644
--- a/tilia/settings.py
+++ b/tilia/settings.py
@@ -89,7 +89,9 @@ class SettingsManager(QObject):
}
def __init__(self):
- self._settings = QSettings(tilia.constants.APP_NAME, "Desktop Settings", None)
+ self._settings = QSettings(
+ tilia.constants.APP_NAME, application="Desktop Settings", parent=None
+ )
self._files_updated_callbacks = set()
self._cache = {}
self._check_all_default_settings_present()
@@ -118,7 +120,10 @@ def link_file_update(self, updating_function) -> None:
def _get(self, group_name: str, setting: str, in_default=True):
key = self._get_key(group_name, setting, in_default)
- value = self._settings.value(key, None)
+ try:
+ value = self._settings.value(key, None)
+ except EOFError:
+ value = None
if not value or not isinstance(
value, type(self.DEFAULT_SETTINGS[group_name][setting])
):
From 804562d545b44564b7557c7c6bccba010f0e6be0 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Thu, 19 Feb 2026 11:59:41 +0000
Subject: [PATCH 54/75] ci: remove CLI from executable
---
tilia.nuitka-package.config.yml | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/tilia.nuitka-package.config.yml b/tilia.nuitka-package.config.yml
index e4c7429ad..6c5353d8c 100644
--- a/tilia.nuitka-package.config.yml
+++ b/tilia.nuitka-package.config.yml
@@ -49,3 +49,11 @@
- description: "slightly hacky way of forcing prod if env file is not found"
replacements_plain:
'os.environ["ENVIRONMENT"] = "dev"': 'os.environ["ENVIRONMENT"] = "prod"'
+
+- module-name: "tilia.boot"
+ anti-bloat:
+ - description: "CLI doesn't currently work in the executable"
+ replacements_re:
+ 'elif interface == "cli":(\n|.)*?return CLI\(\)': ''
+ replacements_plain:
+ 'choices=["qt", "cli"]': 'choices=["qt"], help="CLI option is currently not available in the exe. Run from source instead."'
From d47b9610d1a273d4fab6de7a4f092428fba70e5d Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Feb 2026 17:03:51 +0000
Subject: [PATCH 55/75] ci: test cli
---
.github/workflows/build.yml | 21 ++++++++++++++++++---
.github/workflows/run-tests.yml | 6 +++++-
2 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c0fec5358..243afb478 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -67,12 +67,16 @@ jobs:
echo "Checking dependencies"
# test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is less than 10s.
# then zip everything up and delete the original non-zipped file.
+ echo "Starting GUI..."
start=$(date +%s)
echo "gtimeout 10 ${{ steps.build-exe.outputs.out-filepath }}/Contents/MacOS/${{ steps.build-exe.outputs.out-filename }}" | bash || true
if (($(date +%s) - $start < 10)); then
echo "::error::Process terminated early! Potential errors in build."
exit 1
fi
+ echo "\n\nStarting CLI..."
+ echo "gtimeout 10 ${{ steps.build-exe.outputs.out-filepath }}/Contents/MacOS/${{ steps.build-exe.outputs.out-filename }} --user-interface=cli" | bash || true
+
zip -r9 ${{ steps.build-exe.outputs.zip-filepath }} ${{ steps.build-exe.outputs.out-filepath }} || true
rm -rf ${{ steps.build-exe.outputs.out-filepath }}
@@ -82,12 +86,15 @@ jobs:
run: |
echo "Checking dependencies"
# test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is not 10s.
+ echo "Starting GUI..."
start=$(date +%s)
echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true
if (($(date +%s) - $start < 10)); then
echo "::error::Process terminated early! Potential errors in build."
exit 1
fi
+ echo "\n\nStarting CLI..."
+ echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }} --user-interface=cli" | bash || true
- name: Test executable [Linux]
id: set-path
@@ -97,12 +104,15 @@ jobs:
echo "path=${{ steps.build-exe.outputs.out-filename }}" >> "$GITHUB_OUTPUT"
echo "Checking dependencies"
# test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is not 10s.
+ echo "Starting GUI..."
start=$(date +%s)
echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true
if (($(date +%s) - $start < 10)); then
echo "::error::Process terminated early! Potential errors in build."
exit 1
fi
+ echo "\n\nStarting CLI..."
+ echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }} --user-interface=cli" | bash || true
- name: Upload executable
uses: actions/upload-artifact@v4
@@ -137,13 +147,18 @@ jobs:
run: |
if [ -e build/ubuntu/exe/${{ needs.build.outputs.linux-build }} ];
then
- chmod ugo+x build/ubuntu/exe/${{ needs.build.outputs.linux-build }};
- if echo "timeout 30 build/ubuntu/exe/${{ needs.build.outputs.linux-build }}" | bash;
+ chmod ugo+x build/ubuntu/exe/${{ needs.build.outputs.linux-build }}
+ echo "Starting GUI..."
+ start=$(date +%s)
+ echo "timeout 10 build/ubuntu/exe/${{ needs.build.outputs.linux-build }}" | bash || true
+ if (($(date +%s) - $start < 10));
then
- echo "Linux works in a clean environment!";
+ echo "Linux started in a clean environment!";
else
echo "Linux didn't work... Check libraries with previous step?";
fi;
+ echo "Starting CLI..."
+ echo "timeout 10 build/ubuntu/exe/${{ needs.build.outputs.linux-build }} --user-interface=cli" | bash || true
else
echo "file not found!";
fi
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 40ee4a07a..983ada4f3 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -85,4 +85,8 @@ jobs:
- name: Check Program
shell: bash
- run: timeout 10 tilia-desktop || true
+ run: |
+ echo "Starting GUI..."
+ timeout 10 tilia-desktop || true
+ echo "Starting CLI..."
+ timeout 10 tilia-desktop -i=cli|| true
From 03ecb3c5f9de31628168f7149ac78a5505937f50 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Thu, 19 Feb 2026 15:06:36 +0000
Subject: [PATCH 56/75] fix: correct mode
"app" sets onefile to windows and linux, and bundle to mac.
---
pyproject.toml | 1 +
scripts/deploy.py | 4 ----
2 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 991a86499..ed647f979 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -83,6 +83,7 @@ remove-output = true
report = "compilation-report.xml"
user-package-configuration-file = "tilia.nuitka-package.config.yml"
deployment = true
+mode = "app"
[tool.pytest.ini_options]
env = [
diff --git a/scripts/deploy.py b/scripts/deploy.py
index b7ec4cc4c..8c21216cf 100644
--- a/scripts/deploy.py
+++ b/scripts/deploy.py
@@ -116,10 +116,6 @@ def _get_exe_cmd() -> list[str]:
f"--windows-icon-from-ico={icon_path}",
f"--linux-icon={icon_path}",
]
- if "macos" in build_os:
- exe_args.append("--mode=app")
- else:
- exe_args.append("--mode=onefile")
return exe_args
From 8a0e65295b1ffcc16d67256fc8a0d337e076a2ec Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Thu, 19 Feb 2026 17:15:58 +0000
Subject: [PATCH 57/75] docs: update
no cli in exe
---
README.md | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index 3570e4aa4..20cf428c6 100644
--- a/README.md
+++ b/README.md
@@ -69,47 +69,48 @@ Due to system variations between Linux distributions, some additional system dep
### Running from source
-Clone TiLiA with:
+- Clone TiLiA with:
```
git clone https://github.com/TimeLineAnnotator/desktop.git tilia-desktop
```
-Change directory to the cloned repository:
+- Change directory to the cloned repository:
```
cd tilia-desktop
```
Note: We recommend using a clean [virtual environment](https://docs.python.org/3/library/venv.html) for the next steps.
Failure to do so may cause issues with dependencies.
-Install TiLiA and its dependencies with:
+- Install TiLiA and its dependencies with:
```
pip install -e .
```
-To run TiLiA from source, run:
+- To run TiLiA from source, run:
```
tilia
```
-TiLiA also offers a CLI mode, which can be run with:
+- TiLiA also offers a CLI mode, which can be run with:
```
tilia --user-interface cli
```
+Note: The CLI is currently only available when run from source, and not in the compiled executable.
### Building from source
TiLiA uses [Nuitka](https://nuitka.net/) to build binaries.
Note that the binaries will be for the platform you are building on, as `Nuitka` supports no cross-compilation.
-After cloning TiLiA, install TiLiA's run and build dependencies with:
+- After cloning TiLiA, install TiLiA's run and build dependencies with:
```
pip install -e . --group build
```
-To build a stand-alone executable, run the script:
+- To build a stand-alone executable, run the script:
```
python scripts/deploy.py [ref_name] [os_type]
```
-(*n.b.: `ref_name` and `os_type` are arbitrary strings that do not affect the build outcome.)
+Note: `ref_name` and `os_type` are arbitrary strings that do not affect the build outcome.
The executable will be found in the `build/[os_type]/exe` folder in the project directory.
From 0d15c80ec9897f090e44f47be772e069b95616d3 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Thu, 19 Feb 2026 19:19:12 +0100
Subject: [PATCH 58/75] docs: Fix typos, improve wording & formatting
---
README.md | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index 20cf428c6..0594b12f1 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
-TiLiA (TimeLine Annotator) is a GUI for producing and displaying complex annotations with video and audio files. It is a full-featured but easy-to-use set of tools for researchers and enthusiasts to better analyze their media of interest without needing to rely on textual representations (like music scores). It is written in Python, using the PySide library for its GUI.
+TiLiA (TimeLine Annotator) is a GUI for producing and displaying complex annotations with video and audio files. It is a full-featured, easy-to-use set of tools for researchers and enthusiasts to better analyze their media of interest without needing to rely on textual representations (like music scores). It is written in Python, using the PySide library for its GUI.
TiLiA allows users to annotate media files primarily through timelines of various types. Each one provides different tools and enables specific annotations and visualizations. Currently, there are six types of timelines, but many more are planned.
@@ -11,7 +11,7 @@ TiLiA allows users to annotate media files primarily through timelines of variou
-Here are some examples TiLiA visualizations:
+Here are some examples of TiLiA visualizations:
- Formal analysis of the Piano Sonata in D Major, K.284:
- [First movement](https://tilia-app.com/viewer/135/)
@@ -22,7 +22,7 @@ Here are some examples TiLiA visualizations:
## Current features
- 7 kinds of timelines
- AudioWave: visualize audio files through bars that represent changes in amplitude
- - Beat: beat and measure markers with support to numbering
+ - Beat: beat and measure markers with support for numbering
- Harmony: Roman numeral and chord symbol labels using a specialized font, including proper display of inversion numerals, quality symbols and applied chords
- Hierarchy: nested and levelled units organized in arbitrarily complex hierarchical structures
- Marker: simple, labelled markers to indicate discrete events
@@ -47,7 +47,7 @@ Tutorials on how to use TiLiA can be found on our [website](https://tilia-app.co
## Build or run from source
-TiLiA can be also run and build from source.
+TiLiA can also be run and built from source.
### Prerequisites
@@ -79,8 +79,8 @@ git clone https://github.com/TimeLineAnnotator/desktop.git tilia-desktop
```
cd tilia-desktop
```
-Note: We recommend using a clean [virtual environment](https://docs.python.org/3/library/venv.html) for the next steps.
-Failure to do so may cause issues with dependencies.
+> Note: We recommend using a clean [virtual environment](https://docs.python.org/3/library/venv.html) for the next steps.
+Failure to do so may cause dependency issues.
- Install TiLiA and its dependencies with:
```
@@ -96,7 +96,7 @@ tilia
```
tilia --user-interface cli
```
-Note: The CLI is currently only available when run from source, and not in the compiled executable.
+> Note: The CLI is currently only available when run from source, and not in the compiled executable.
### Building from source
TiLiA uses [Nuitka](https://nuitka.net/) to build binaries.
@@ -110,7 +110,7 @@ pip install -e . --group build
```
python scripts/deploy.py [ref_name] [os_type]
```
-Note: `ref_name` and `os_type` are arbitrary strings that do not affect the build outcome.
+> Note: `ref_name` and `os_type` are arbitrary strings that do not affect the build outcome.
The executable will be found in the `build/[os_type]/exe` folder in the project directory.
From 7a4ad026943adf25160132f66ec9acc978ad07ed Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Fri, 20 Feb 2026 15:28:08 +0000
Subject: [PATCH 59/75] fix: improve debug message
---
tilia/__main__.py | 59 ++++++++++++++++++++++++++++-------------------
1 file changed, 35 insertions(+), 24 deletions(-)
diff --git a/tilia/__main__.py b/tilia/__main__.py
index 9b6c5dba8..e769ab26c 100644
--- a/tilia/__main__.py
+++ b/tilia/__main__.py
@@ -9,41 +9,52 @@
def deps_debug(exc: ImportError):
+ from tilia.constants import EMAIL, GITHUB_URL, WEBSITE_URL # noqa: E402
+
+ def _raise_deps_error(exc: ImportError, message: list[str]):
+ raise RuntimeError("\n".join(message)) from exc
+
+ distro = platform.freedesktop_os_release().get("ID_LIKE", "").split()[0]
+ link = f"{WEBSITE_URL}/help/installation?distro={distro}#troubleshooting-linux"
root_path = Path(
[*traceback.walk_tb(exc.__traceback__)][0][0].f_code.co_filename
).parent
lib_path = root_path / "PySide6/qt-plugins/platforms/libqxcb.so"
+
if not lib_path.exists():
- print(
- "Could not find libqxcb.so file.\nDumping all files in tree for debug...\n",
- subprocess.getoutput(f"ls {root_path.as_posix()} -ltraR"),
- )
- raise RuntimeError(
- "Could not locate the necessary libraries to run TiLiA."
- ) from exc
-
- _, result = subprocess.getstatusoutput(f"ldd {lib_path.as_posix()}")
- if "=> not found" in result:
+ if "__compiled__" not in globals():
+ msg = [
+ "Could not locate the necessary libraries to run TiLiA. libqxcb.so file not found.",
+ "Did you forget to install python dependencies?",
+ ]
+ else:
+ msg = [
+ "Could not locate the necessary libraries to run TiLiA. libqxcb.so file not found.",
+ f"Open an issue on our repo at <{GITHUB_URL}> or contact us at <{EMAIL}> for help.\n"
+ "Dumping all files in tree for debug...",
+ subprocess.getoutput(f"ls {root_path.as_posix()} -ltraR"),
+ ]
+ _raise_deps_error(exc, msg)
+
+ deps = subprocess.getoutput(f"ldd {lib_path.as_posix()}")
+ if "=> not found" in deps:
missing_deps = []
- for line in result.splitlines():
+ for line in deps.splitlines():
if "=> not found" in line:
dep = line.strip().rstrip(" => not found")
missing_deps.append(dep)
- from tilia.constants import WEBSITE_URL # noqa: E402
-
- distro = platform.freedesktop_os_release().get("ID_LIKE", "").split()[0]
- link = f"{WEBSITE_URL}/help/installation?distro={distro}#troubleshooting-linux"
if missing_deps:
- print(
- f"""TiLiA could not start due to missing system dependencies.
-Visit <{link}> for help on installation.
-Missing libraries:
-{missing_deps}"""
- )
- webbrowser.open(link)
-
- raise RuntimeError("Install the necessary dependencies then restart.") from exc
+ deps = f"Missing libraries:\n{missing_deps}"
+
+ msg = [
+ "TiLiA could not start due to missing system dependencies.",
+ f"Visit <{link}> for help on installation.",
+ "Install the necessary dependencies then restart.\n",
+ deps,
+ ]
+ webbrowser.open(link)
+ _raise_deps_error(exc, msg)
def main():
From 04e2d28ee08bee19264c875e9dd6262db73c6ace Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Fri, 20 Feb 2026 17:11:14 +0000
Subject: [PATCH 60/75] docs: some explanatory notes
---
pyproject.toml | 2 +-
tilia/settings.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index ed647f979..07a377ff1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,7 +49,7 @@ build = [
"Nuitka >= 4.0",
"ordered-set==4.1.0",
"pyyaml",
- "setuptools<82.0.0; python_version >= '3.12'",
+ "setuptools<82.0.0; python_version >= '3.12'", # setuptools>82.0.0 no longer provides pkg_resources
"wheel==0.38.4",
"zstandard==0.20.0"
]
diff --git a/tilia/settings.py b/tilia/settings.py
index ef597e3fb..386d22331 100644
--- a/tilia/settings.py
+++ b/tilia/settings.py
@@ -122,7 +122,7 @@ def _get(self, group_name: str, setting: str, in_default=True):
key = self._get_key(group_name, setting, in_default)
try:
value = self._settings.value(key, None)
- except EOFError:
+ except EOFError: # happens when the group in self._settings is not initiated, but setting a value solves this.
value = None
if not value or not isinstance(
value, type(self.DEFAULT_SETTINGS[group_name][setting])
From e1d87c0076a24a6681e41530ffa10a5bc6ed80f8 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Fri, 20 Feb 2026 17:11:29 +0000
Subject: [PATCH 61/75] fix: typo
---
.github/workflows/run-tests.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 983ada4f3..9f7d4dff2 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -87,6 +87,6 @@ jobs:
shell: bash
run: |
echo "Starting GUI..."
- timeout 10 tilia-desktop || true
+ timeout 10 tilia || true
echo "Starting CLI..."
- timeout 10 tilia-desktop -i=cli|| true
+ timeout 10 tilia -i=cli|| true
From 9b2a6bb0d06259688fffba48af7100251a613f47 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Fri, 20 Feb 2026 17:41:48 +0000
Subject: [PATCH 62/75] ci: fix and standardise
---
.github/workflows/run-tests.yml | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 9f7d4dff2..60b311784 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -38,7 +38,7 @@ jobs:
python-version: '3.13' # no pyside support
timeout-minutes: 30
env:
- QT_SELECT: "qt6"
+ QT_QPA_PLATFORM: offscreen
steps:
- uses: actions/checkout@v3
@@ -47,12 +47,15 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- - name: Setup xvfb (Linux)
+ - name: Setup additional Linux dependencies
if: runner.os == 'Linux'
run: |
- sudo apt-get update
- sudo apt-get install -y xvfb libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xinput0 libxcb-xfixes0 libxcb-shape0 libglib2.0-0 libgl1-mesa-dev libpulse-dev
- sudo apt-get install '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev
+ chmod +x ./scripts/linux-setup.sh
+ ./scripts/linux-setup.sh
+
+ - name: Install timeout
+ if: runner.os == 'macOS'
+ run: brew install coreutils
- uses: actions/cache@v4
with:
@@ -86,7 +89,8 @@ jobs:
- name: Check Program
shell: bash
run: |
+ export QT_DEBUG_PLUGINS=1
echo "Starting GUI..."
timeout 10 tilia || true
echo "Starting CLI..."
- timeout 10 tilia -i=cli|| true
+ timeout 10 tilia -i=cli || true
From 5474b6c3723da083e159395fb6a5dc881b9e9bfc Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Wed, 4 Mar 2026 12:41:49 +0000
Subject: [PATCH 63/75] fix: typo
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0594b12f1..105121ea5 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
TiLiA (TimeLine Annotator) is a GUI for producing and displaying complex annotations with video and audio files. It is a full-featured, easy-to-use set of tools for researchers and enthusiasts to better analyze their media of interest without needing to rely on textual representations (like music scores). It is written in Python, using the PySide library for its GUI.
-TiLiA allows users to annotate media files primarily through timelines of various types. Each one provides different tools and enables specific annotations and visualizations. Currently, there are six types of timelines, but many more are planned.
+TiLiA allows users to annotate media files primarily through timelines of various types. Each one provides different tools and enables specific annotations and visualizations. Currently, there are seven types of timelines, but many more are planned.
From a78cee15c1d3ad07a80f712a61a23ffd62428ab6 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Wed, 4 Mar 2026 12:44:08 +0000
Subject: [PATCH 64/75] fix: correct location
---
scripts/deploy.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/deploy.py b/scripts/deploy.py
index 8c21216cf..264aec7be 100644
--- a/scripts/deploy.py
+++ b/scripts/deploy.py
@@ -156,7 +156,7 @@ def _create_lib() -> Path:
filter=lambda x, _: (
x
if x.name.startswith(tilia)
- or x.name.startswith("TiLiA.egg-info")
+ or x.name.startswith(f"{base}/TiLiA.egg-info")
or x.name in ext_data
else None
),
From db963064401af005fadbb715d2e9b30357871833 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Mon, 16 Mar 2026 16:46:30 +0000
Subject: [PATCH 65/75] ci: bump actions version
Node.js 20 deprecation
---
.github/workflows/build.yml | 10 +++++-----
.github/workflows/run-tests.yml | 6 +++---
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 243afb478..dee15306c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -28,10 +28,10 @@ jobs:
QT_QPA_PLATFORM: offscreen
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Set up Python 3.11
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: 3.11
cache: "pip"
@@ -115,7 +115,7 @@ jobs:
echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }} --user-interface=cli" | bash || true
- name: Upload executable
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: ${{ steps.build-exe.outputs.out-filename }}
path: build/*/exe
@@ -132,10 +132,10 @@ jobs:
QT_DEBUG_PLUGINS: 1
QT_QPA_PLATFORM: offscreen
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Download executable
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v8
with:
path: build/
pattern: 'TiLiA-v*'
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 60b311784..1c95dcb4e 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -41,9 +41,9 @@ jobs:
QT_QPA_PLATFORM: offscreen
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
@@ -57,7 +57,7 @@ jobs:
if: runner.os == 'macOS'
run: brew install coreutils
- - uses: actions/cache@v4
+ - uses: actions/cache@v5
with:
path: ${{ matrix.path }}
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
From a5bcdd93152cdd6368a4788abf10b4f7acfcfb27 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Tue, 17 Mar 2026 10:33:48 +0000
Subject: [PATCH 66/75] ci: don't zip mac
wan't doing anything to improve the file size
---
.github/workflows/build.yml | 3 ---
scripts/deploy.py | 4 ----
2 files changed, 7 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index dee15306c..80de32e0e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -77,9 +77,6 @@ jobs:
echo "\n\nStarting CLI..."
echo "gtimeout 10 ${{ steps.build-exe.outputs.out-filepath }}/Contents/MacOS/${{ steps.build-exe.outputs.out-filename }} --user-interface=cli" | bash || true
- zip -r9 ${{ steps.build-exe.outputs.zip-filepath }} ${{ steps.build-exe.outputs.out-filepath }} || true
- rm -rf ${{ steps.build-exe.outputs.out-filepath }}
-
- name: Test executable [Windows]
if: runner.os == 'Windows'
shell: bash
diff --git a/scripts/deploy.py b/scripts/deploy.py
index 264aec7be..8943e5099 100644
--- a/scripts/deploy.py
+++ b/scripts/deploy.py
@@ -261,10 +261,6 @@ def build():
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"out-filepath={out_filepath.as_posix()}\n")
f.write(f"out-filename={out_filename}\n")
- if "mac" in build_os:
- f.write(
- f"zip-filepath={outdir.as_posix()}/exe/{out_filename}.zip\n"
- )
os.chdir(old_dir)
dotenv.set_key(".tilia.env", "ENVIRONMENT", old_env_var)
except Exception as e:
From 703a523f66971413cf668f3c3e71218340efb658 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Tue, 17 Mar 2026 19:26:54 +0000
Subject: [PATCH 67/75] fi: correct filename
---
scripts/deploy.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/scripts/deploy.py b/scripts/deploy.py
index 8943e5099..97202952d 100644
--- a/scripts/deploy.py
+++ b/scripts/deploy.py
@@ -255,9 +255,8 @@ def build():
_build_exe()
if os.environ.get("GITHUB_OUTPUT"):
if "mac" in build_os:
- out_filepath = outdir / "exe" / "tilia.app"
- else:
- out_filepath = outdir / "exe" / out_filename
+ os.rename(outdir / "exe" / "tilia.app", outdir / "exe" / out_filename)
+ out_filepath = outdir / "exe" / out_filename
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"out-filepath={out_filepath.as_posix()}\n")
f.write(f"out-filename={out_filename}\n")
From 0d3dc9abb3da5a8e44dc9d149ad62175523f4a51 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Wed, 18 Mar 2026 01:53:39 +0000
Subject: [PATCH 68/75] fix: correct filename
---
scripts/deploy.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/scripts/deploy.py b/scripts/deploy.py
index 97202952d..0a2ecc456 100644
--- a/scripts/deploy.py
+++ b/scripts/deploy.py
@@ -255,7 +255,10 @@ def build():
_build_exe()
if os.environ.get("GITHUB_OUTPUT"):
if "mac" in build_os:
- os.rename(outdir / "exe" / "tilia.app", outdir / "exe" / out_filename)
+ os.rename(
+ outdir / "exe" / "tilia.app",
+ outdir / "exe" / (out_filename + ".app"),
+ )
out_filepath = outdir / "exe" / out_filename
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"out-filepath={out_filepath.as_posix()}\n")
From ef849442d108dc4cbea06338f5d9b553541006a9 Mon Sep 17 00:00:00 2001
From: azfoo <45888544+azfoo@users.noreply.github.com>
Date: Wed, 18 Mar 2026 13:15:33 +0000
Subject: [PATCH 69/75] fix: correct filename
---
scripts/deploy.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/scripts/deploy.py b/scripts/deploy.py
index 0a2ecc456..10b0461d3 100644
--- a/scripts/deploy.py
+++ b/scripts/deploy.py
@@ -259,7 +259,9 @@ def build():
outdir / "exe" / "tilia.app",
outdir / "exe" / (out_filename + ".app"),
)
- out_filepath = outdir / "exe" / out_filename
+ out_filepath = outdir / "exe" / (out_filename + ".app")
+ else:
+ out_filepath = outdir / "exe" / out_filename
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"out-filepath={out_filepath.as_posix()}\n")
f.write(f"out-filename={out_filename}\n")
From 82473ce66e10d519abfc17bb98dd13067a623852 Mon Sep 17 00:00:00 2001
From: Felipe Martins
Date: Fri, 20 Mar 2026 13:15:21 +0100
Subject: [PATCH 70/75] fix: handle tilia package metadata not found
---
tests/test_constants.py | 24 ++++++++++++++++++++++++
tilia/constants.py | 25 +++++++++++++++----------
2 files changed, 39 insertions(+), 10 deletions(-)
create mode 100644 tests/test_constants.py
diff --git a/tests/test_constants.py b/tests/test_constants.py
new file mode 100644
index 000000000..ccee02720
--- /dev/null
+++ b/tests/test_constants.py
@@ -0,0 +1,24 @@
+import importlib.metadata
+import sys
+from unittest.mock import patch
+
+
+def test_tilia_metadata_not_found():
+ # If tilia.constants was already imported, we must remove it from sys.modules
+ # to ensure the module-level code runs again with our mocks.
+ if "tilia.constants" in sys.modules:
+ del sys.modules["tilia.constants"]
+
+ # 1. Mock Path.exists to return False so it skips the pyproject.toml logic
+ with patch("pathlib.Path.exists", return_value=False), patch(
+ "importlib.metadata.metadata",
+ side_effect=importlib.metadata.PackageNotFoundError,
+ ):
+
+ import tilia.constants as constants
+
+ assert constants.APP_NAME == ""
+ assert constants.VERSION == "0.0.0"
+ assert constants.YEAR == "2022-2026"
+ assert constants.AUTHOR == ""
+ assert constants.EMAIL == ""
diff --git a/tilia/constants.py b/tilia/constants.py
index 6324dfc98..550b09ab4 100644
--- a/tilia/constants.py
+++ b/tilia/constants.py
@@ -16,16 +16,21 @@
EMAIL = setupcfg.get("authors", [{"email": ""}])[0]["email"]
else:
- setupcfg = metadata.metadata("TiLiA").json.copy()
-
- AUTHOR = re.search(r'"(.*?)"', setupcfg.get("author_email", "")).group(1)
- EMAIL = re.search(r"<(.*?)>", setupcfg.get("author_email", "")).group(1)
- if "urls" not in setupcfg:
- setupcfg["urls"] = {}
- for url in setupcfg.get("project_url", {}):
- k, _, v = url.partition(", ")
- setupcfg["urls"][k] = v
- setupcfg["description"] = setupcfg.get("summary", "")
+ try:
+ setupcfg = metadata.metadata("TiLiA").json.copy()
+
+ AUTHOR = re.search(r'"(.*?)"', setupcfg.get("author_email", "")).group(1)
+ EMAIL = re.search(r"<(.*?)>", setupcfg.get("author_email", "")).group(1)
+ if "urls" not in setupcfg:
+ setupcfg["urls"] = {}
+ for url in setupcfg.get("project_url", {}):
+ k, _, v = url.partition(", ")
+ setupcfg["urls"][k] = v
+ setupcfg["description"] = setupcfg.get("summary", "")
+ except metadata.PackageNotFoundError:
+ setupcfg = {}
+ AUTHOR = ""
+ EMAIL = ""
APP_NAME = setupcfg.get("name", "")
VERSION = setupcfg.get("version", "0.0.0")
From e2bf10e31e0d5a9fe58c5be5881cf34472fb09a0 Mon Sep 17 00:00:00 2001
From: Felipe Martins
Date: Fri, 20 Mar 2026 16:23:55 +0100
Subject: [PATCH 71/75] fix: update event name in EXCLUDE_FROM_LOG event name
---
.tilia.env | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.tilia.env b/.tilia.env
index e88594eb0..ffbaed9d8 100644
--- a/.tilia.env
+++ b/.tilia.env
@@ -1,3 +1,3 @@
LOG_REQUESTS = 1
-EXCLUDE_FROM_LOG = TIMELINE_VIEW_LEFT_BUTTON_DRAG;PLAYER_CURRENT_TIME_CHANGED;APP_RECORD_STATE
+EXCLUDE_FROM_LOG = TIMELINE_VIEW_LEFT_BUTTON_DRAG;PLAYER_CURRENT_TIME_CHANGED;APP_STATE_RECORD
ENVIRONMENT='dev'
From 456c82fac46622455130d1b2447463d7af66424d Mon Sep 17 00:00:00 2001
From: Felipe Martins
Date: Fri, 20 Mar 2026 16:26:05 +0100
Subject: [PATCH 72/75] fix: circular import
---
tilia/ui/timelines/collection/collection.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tilia/ui/timelines/collection/collection.py b/tilia/ui/timelines/collection/collection.py
index 05ae297f4..befb78670 100644
--- a/tilia/ui/timelines/collection/collection.py
+++ b/tilia/ui/timelines/collection/collection.py
@@ -41,8 +41,6 @@
from tilia.ui.timelines.view import TimelineView
from .view import TimelineUIsView
from ..base.element import TimelineUIElement
-from ..beat import BeatTimelineUI
-from ..harmony import HarmonyTimelineUI
from ..selection_box import SelectionBoxQt
from ..slider.timeline import SliderTimelineUI
from ...dialogs.add_timeline_without_media import AddTimelineWithoutMedia
@@ -916,6 +914,8 @@ def on_hierarchy_merge_split(self, new_units: list, old_units: list):
self._update_loop_elements()
def on_harmony_timeline_components_deserialized(self, id):
+ from ..harmony import HarmonyTimelineUI
+
timeline_ui = cast(HarmonyTimelineUI, self.get_timeline_ui(id))
timeline_ui.on_timeline_components_deserialized()
From a334b63853e0b526279adebe776d01727e9b20fe Mon Sep 17 00:00:00 2001
From: Felipe Martins
Date: Fri, 20 Mar 2026 16:29:32 +0100
Subject: [PATCH 73/75] fix: use dict.get(key) instead of dict[key]
---
tilia/requests/post.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/tilia/requests/post.py b/tilia/requests/post.py
index 9f78aba5f..bf70f9715 100644
--- a/tilia/requests/post.py
+++ b/tilia/requests/post.py
@@ -111,9 +111,7 @@ class Post(Enum):
def _log_post(post, *args, **kwargs):
- log_message = (
- f"{post.name:<40} {str((args, kwargs)):<100} {list(_posts_to_listeners[post])}"
- )
+ log_message = f"{post.name:<40} {str((args, kwargs)):<100} {list(_posts_to_listeners.get(post, ''))}"
if post is Post.DISPLAY_ERROR:
logger.warning(log_message)
return
From 55dbf3cb7f210c20e1182aa2a95ec43cce7369ed Mon Sep 17 00:00:00 2001
From: Felipe Martins
Date: Fri, 20 Mar 2026 16:30:13 +0100
Subject: [PATCH 74/75] test: fix error when tearing down ManageTimelines()
---
tests/ui/windows/test_manage_timelines.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/tests/ui/windows/test_manage_timelines.py b/tests/ui/windows/test_manage_timelines.py
index e0cec94b1..9604609f5 100644
--- a/tests/ui/windows/test_manage_timelines.py
+++ b/tests/ui/windows/test_manage_timelines.py
@@ -20,7 +20,9 @@ def assert_order_is_correct(tls: Timelines, expected: list[Timeline]):
# assert list widget order
for i, tl in enumerate(expected):
tlui = get(Get.TIMELINE_UI, tl.id)
- assert ManageTimelines().list_widget.item(i).timeline_ui == tlui
+ mt = ManageTimelines()
+ assert mt.list_widget.item(i).timeline_ui == tlui
+ mt.close()
class TestChangeTimelineVisibility:
From fdd65ff8918f8f528bb63f3ac6bb8f39edb500e2 Mon Sep 17 00:00:00 2001
From: Felipe Martins
Date: Fri, 20 Mar 2026 16:50:45 +0100
Subject: [PATCH 75/75] test: timeline is deleted while ManagaTimelines is open
---
tests/ui/windows/test_manage_timelines.py | 52 ++++++++++++++-------
tilia/ui/timelines/collection/collection.py | 8 ++--
2 files changed, 38 insertions(+), 22 deletions(-)
diff --git a/tests/ui/windows/test_manage_timelines.py b/tests/ui/windows/test_manage_timelines.py
index 9604609f5..3c246718c 100644
--- a/tests/ui/windows/test_manage_timelines.py
+++ b/tests/ui/windows/test_manage_timelines.py
@@ -1,3 +1,4 @@
+from contextlib import contextmanager
from typing import Literal
import pytest
@@ -20,8 +21,17 @@ def assert_order_is_correct(tls: Timelines, expected: list[Timeline]):
# assert list widget order
for i, tl in enumerate(expected):
tlui = get(Get.TIMELINE_UI, tl.id)
- mt = ManageTimelines()
- assert mt.list_widget.item(i).timeline_ui == tlui
+ with manage_timelines() as mt:
+ assert mt.list_widget.item(i).timeline_ui == tlui
+
+
+@contextmanager
+def manage_timelines():
+ """Context manager for the ManageTimelines window."""
+ mt = ManageTimelines()
+ try:
+ yield mt
+ finally:
mt.close()
@@ -29,10 +39,9 @@ class TestChangeTimelineVisibility:
@staticmethod
def toggle_timeline_is_visible(row: int = 0):
"""Toggles timeline visibility using the Manage Timelines window."""
- mt = ManageTimelines()
- mt.list_widget.setCurrentRow(row)
- QTest.mouseClick(mt.checkbox, Qt.MouseButton.LeftButton)
- mt.close()
+ with manage_timelines() as mt:
+ mt.list_widget.setCurrentRow(row)
+ QTest.mouseClick(mt.checkbox, Qt.MouseButton.LeftButton)
def test_hide(self, marker_tlui):
commands.execute("timeline.set_is_visible", marker_tlui, True)
@@ -66,17 +75,16 @@ def setup_timelines(self, tluis, tls):
@staticmethod
def click_set_ordinal_button(button: Literal["up", "down"], row: int):
"""Toggles timeline visibility using the ManageTimelines window."""
- mt = ManageTimelines()
- mt.list_widget.setCurrentRow(row)
- if button == "up":
- button = mt.up_button
- elif button == "down":
- button = mt.down_button
- else:
- assert False, "Invalid button value."
-
- QTest.mouseClick(button, Qt.MouseButton.LeftButton)
- mt.close()
+ with manage_timelines() as mt:
+ mt.list_widget.setCurrentRow(row)
+ if button == "up":
+ button = mt.up_button
+ elif button == "down":
+ button = mt.down_button
+ else:
+ assert False, "Invalid button value."
+
+ QTest.mouseClick(button, Qt.MouseButton.LeftButton)
def test_increase_ordinal(self, tls, setup_timelines):
tl0, tl1, tl2 = setup_timelines
@@ -143,3 +151,13 @@ def test_decrease_ordinal_with_last_selected_does_nothing(
self.click_set_ordinal_button("down", 2)
assert_order_is_correct(tls, [tl0, tl1, tl2])
+
+
+class TesttimelinesChangeWhileOpen:
+ def test_timeline_is_deleted(self, tluis):
+ commands.execute("timelines.add.marker", name="")
+ with manage_timelines() as mt:
+ mt.list_widget.setCurrentRow(0)
+ commands.execute("timeline.delete", tluis[0], confirm=False)
+
+ # Much more could be tested here.
diff --git a/tilia/ui/timelines/collection/collection.py b/tilia/ui/timelines/collection/collection.py
index befb78670..9dfa0a9ac 100644
--- a/tilia/ui/timelines/collection/collection.py
+++ b/tilia/ui/timelines/collection/collection.py
@@ -1132,14 +1132,12 @@ def on_timeline_ordinal_permute(self, tlui1: TimelineUI, tlui2: TimelineUI):
@staticmethod
@command_callback
- def on_timeline_delete(timeline_ui: TimelineUI):
- confirmed = get(
+ def on_timeline_delete(timeline_ui: TimelineUI, confirm: bool = True) -> bool:
+ if confirm and not get(
Get.FROM_USER_YES_OR_NO,
"Delete timeline",
"Are you sure you want to delete the selected timeline? This can be undone later.",
- )
-
- if not confirmed:
+ ):
return False
get(Get.TIMELINE_COLLECTION).delete_timeline(timeline_ui.timeline)