working on openning and closing files

This commit is contained in:
2026-01-15 16:24:43 +11:00
parent 353c230d15
commit 1cdd04f67b
9 changed files with 269 additions and 15 deletions

View File

@@ -2,6 +2,7 @@ pymp3
textual
textual-slider
textual-plot
textual-fspicker
numpy
msgpack-numpy
pedalboard

View File

@@ -65,7 +65,7 @@ if __name__ == "__main__":
test_project.channels.append(piano_channel)
test_project.write_to_file("test_project.tdp")"""
test_project = Project.from_file("test_project.tdp")
test_project = Project()#.from_file("test_project.tdp")
# start the ui
print("Starting UI...")

Binary file not shown.

View File

@@ -2,12 +2,15 @@ from textual.app import App, ComposeResult
from textual.widgets import Footer, Tab, Tabs, Header
from textual import on, events
from textual_fspicker import FileOpen, FileSave, Filters
from ui.widgets.sidebar import Sidebar
from ui.widgets.timeline import Timeline, TimelineRow
from ui.widgets.project_settings import ProjectSettings
from ui.widgets.channel import Channel
from ui.widgets.context_menu import ContextMenu, NoSelectStatic
from project import ProjectChannel
from project import ProjectChannel, Project
class AppUI(App):
@@ -19,6 +22,9 @@ class AppUI(App):
self.last_zoom_level = self.zoom_level
self.project = project
self.open_project_path = None
self.first_tab_click = True # stupid events firing when the app is first composed :/
@on(events.Key)
async def key_pressed(self, event: events.Key):
if event.key == "space":
@@ -29,22 +35,140 @@ class AppUI(App):
timeline.song_player.play_project(self.app.project)
def create_channel(self, name: str):
new_project_channel = ProjectChannel(
name
)
self.query_one("#channels").mount(Channel(
len(self.project.channels),
new_project_channel,
name,
), before=-1)
self.query_one("#rows").mount(TimelineRow())
self.query_one("#rows").mount(TimelineRow(new_project_channel))
self.project.channels.append(ProjectChannel(
name
))
self.project.channels.append(new_project_channel)
def handle_menu_click(self, choice: str):
# user clicked off the menu
if choice == None: return
match choice:
case "Save":
if not self.open_project_path:
self.handle_menu_click("Save as") # just move it to save as
return
self.project.write_to_file(self.open_project_path)
self.notify("Saved project.")
case "Save as":
def callback(path: str):
if path == None: return
self.open_project_path = path
self.project.write_to_file(path)
self.notify(f"Saved as \"{path}\"", title="Saved")
self.push_screen(FileSave(
filters=Filters(
("Any", lambda _: True),
("TerminalDAW Project File", lambda p: p.suffix.lower() == ".tdp")
)
), callback=callback)
case "Open":
def callback(path: str):
if path == None: return
self.project = Project.from_file(path)
self.open_project_path = path
### load all the ui elements
# sidebar channels
sidebar_channels = self.query_one("#channels")
# we cant use sidebar_channels.remove_children() because that would
# also remove the "Add Channel" button
for child in sidebar_channels.children:
if child.id != "add-channel":
child.remove()
for i, channel in enumerate(self.app.project.channels):
sidebar_channels.mount(Channel(
i,
channel,
channel.name,
channel.mute,
channel.solo,
channel.pan,
channel.volume
), before=-1)
# timeline tracks
timeline = self.query_one(Timeline)
rows = timeline.query_one("#rows")
for channel in self.project.channels:
row = TimelineRow(channel)
row.styles.width = timeline.bar_offset * self.project.song_length
for chunk in channel.chunks:
if chunk.chunk_type == ChunkType.CHUNK:
row.mount(Chunk(chunk_name=chunk.name, bar_pos=chunk.position))
elif chunk.chunk_type == ChunkType.AUDIO:
row.mount(AudioChunk(chunk.audio_data, chunk.sample_rate, chunk.name, chunk.position))
rows.mount(row)
self.notify(f"Loaded \"{path}\"")
self.push_screen(FileOpen(
filters=Filters(
("Any", lambda _: True),
("TerminalDAW Project File", lambda p: p.suffix.lower() == ".tdp")
)
), callback=callback)
case _:
self.notify("Sorry, that isn't implemented yet... ;-;", severity="warning")
@on(Tabs.TabActivated)
async def top_menu_tab_clicked(self, event: Tabs.TabActivated):
if self.first_tab_click:
event.tabs.active = None
self.first_tab_click = False
return
options = {
"File": [
"Save",
"Save as",
"Open",
"Render",
"Settings"
],
"Edit": [
"Copy",
"Paste",
"Cut"
],
"About": [
"Chookspace repo",
"Copyright"
]
}
self.app.push_screen(ContextMenu(options[event.tab.label], self.app.mouse_position + (0, 1)), callback=self.handle_menu_click)
def compose(self) -> ComposeResult:
with Tabs(id="top-menu"):
yield Tab("File")
yield Tab("Edit")
yield Tab("View")
yield Tab("Help")
yield Tabs(
Tab("File"),
Tab("Edit"),
Tab("About"),
id="top-menu")
yield Sidebar()
yield Timeline()
yield ProjectSettings()

View File

@@ -6,6 +6,8 @@ from textual_slider import Slider
from textual import on, events
from project import ProjectChannel
class Channel(VerticalGroup):
DEFAULT_CSS = """
@@ -46,9 +48,10 @@ class Channel(VerticalGroup):
}
"""
def __init__(self, channel_index: int, channel_name: str = "", muted: bool = False, solo: bool = False, pan: float = 0, volume: float = 0):
def __init__(self, channel_index: int, project_channel: ProjectChannel, channel_name: str = "", muted: bool = False, solo: bool = False, pan: float = 0, volume: float = 0):
super().__init__()
self.channel_index = channel_index
self.project_channel = project_channel
self.channel_name = channel_name
self.muted = muted
self.solo = solo

View File

@@ -0,0 +1,119 @@
# totally not stolen from my code for my chat app Portal ;)
from __future__ import annotations
from textual.screen import ModalScreen
from textual.containers import Container
from textual.widgets import Static
from textual.geometry import Offset
from textual.message import Message
from textual.visual import VisualType
from textual import on, events
class NoSelectStatic(Static):
"""This class is used in window.py and windowbar.py to create buttons."""
@property
def allow_select(self) -> bool:
return False
class ButtonStatic(NoSelectStatic):
"""This class is used in window.py, windowbar.py, and switcher.py to create buttons."""
class Pressed(Message):
def __init__(self, button: ButtonStatic) -> None:
super().__init__()
self.button = button
@property
def control(self) -> ButtonStatic:
return self.button
def __init__(
self,
content: VisualType = "",
*,
expand: bool = False,
shrink: bool = False,
markup: bool = True,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
) -> None:
super().__init__(
content=content,
expand=expand,
shrink=shrink,
markup=markup,
name=name,
id=id,
classes=classes,
disabled=disabled,
)
self.click_started_on: bool = False
def on_mouse_down(self, event: events.MouseDown) -> None:
self.add_class("pressed")
self.click_started_on = True
def on_mouse_up(self, event: events.MouseUp) -> None:
self.remove_class("pressed")
if self.click_started_on:
self.post_message(self.Pressed(self))
self.click_started_on = False
def on_leave(self, event: events.Leave) -> None:
self.remove_class("pressed")
self.click_started_on = False
class ContextMenu(ModalScreen):
DEFAULT_CSS = """
ContextMenu {
background: $background 0%;
align: left top;
}
#menu_container {
padding: 0 1;
background: $surface;
width: 21;
border: tall $surface-lighten-1;
& > ButtonStatic {
content-align: left middle;
&:hover { background: $panel-lighten-2; }
&.pressed { background: $primary; }
}
}
"""
def __init__(self, options: list[str], offset: Offset):
super().__init__()
self.options = options
self.mouse_offset = offset
def on_mouse_up(self, event: events.MouseUp):
if not self.query_one("#menu_container").region.contains(event.screen_x, event.screen_y):
self.dismiss(None)
@on(ButtonStatic.Pressed)
async def thingy(self, event: ButtonStatic.Pressed):
self.dismiss(event.button.content)
def compose(self):
with Container(id="menu_container"):
for option in self.options:
if isinstance(option, str):
yield ButtonStatic(option)
else:
yield option
def on_mount(self):
menu_container = self.query_one("#menu_container")
menu_container.styles.height = len(menu_container.children) + 2
menu_container.offset = self.mouse_offset

View File

@@ -16,9 +16,12 @@ class ProjectSettings(Horizontal):
align-vertical: middle;
padding: 0 1;
Button {
#play-button {
max-width: 5;
}
#tempo-tap {
max-width: 7;
}
#song-bpm {
max-width: 12;
@@ -54,6 +57,7 @@ class ProjectSettings(Horizontal):
yield Static(" BPM: ")
yield Input("120", placeholder="120", valid_empty=False, type="integer", max_length=3, id="song-bpm")
yield Button("TAP", flat=True, id="tempo-tap")
yield Static(" Time Signature: ")
yield Input("4/4", placeholder="4/4", valid_empty=False, id="song-time-sig")

View File

@@ -26,6 +26,10 @@ class TimelineRow(Horizontal):
}
}
"""
def __init__(self, project_channel):
super().__init__()
self.project_channel = project_channel
class Timeline(Vertical):
DEFAULT_CSS = """
@@ -126,7 +130,7 @@ class Timeline(Vertical):
with VerticalScroll(id="rows"):
for channel in self.app.project.channels:
with TimelineRow() as row:
with TimelineRow(channel) as row:
row.styles.width = self.bar_offset * self.app.project.song_length

View File

@@ -1 +0,0 @@
from textual.widgets import Tabs, Tab