2026-01-13 16:06:57 +11:00
|
|
|
from textual.app import App, ComposeResult
|
2026-01-14 14:43:57 +11:00
|
|
|
from textual.widgets import Footer, Tab, Tabs, Header
|
2026-01-15 07:58:09 +11:00
|
|
|
from textual import on, events
|
2026-01-13 16:06:57 +11:00
|
|
|
|
2026-01-15 16:24:43 +11:00
|
|
|
from textual_fspicker import FileOpen, FileSave, Filters
|
|
|
|
|
|
2026-01-13 16:06:57 +11:00
|
|
|
from ui.widgets.sidebar import Sidebar
|
2026-01-14 15:51:34 +11:00
|
|
|
from ui.widgets.timeline import Timeline, TimelineRow
|
2026-01-13 21:33:25 +11:00
|
|
|
from ui.widgets.project_settings import ProjectSettings
|
2026-01-14 15:51:34 +11:00
|
|
|
from ui.widgets.channel import Channel
|
2026-01-15 16:24:43 +11:00
|
|
|
from ui.widgets.context_menu import ContextMenu, NoSelectStatic
|
2026-01-18 07:04:54 +11:00
|
|
|
from ui.widgets.chunk_types.audio import AudioChunk, Chunk
|
|
|
|
|
from ui.screens.settings import SettingsScreen
|
2026-01-14 15:51:34 +11:00
|
|
|
|
2026-01-16 08:26:15 +11:00
|
|
|
from project import ProjectChannel, Project, ChunkType
|
2026-01-18 07:04:54 +11:00
|
|
|
from settings_store import ConfigHandler
|
|
|
|
|
|
2026-01-13 16:06:57 +11:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class AppUI(App):
|
2026-01-14 10:31:26 +11:00
|
|
|
CSS_PATH = "../assets/style.tcss"
|
|
|
|
|
|
2026-01-14 09:11:09 +11:00
|
|
|
def __init__(self, project):
|
2026-01-13 20:06:28 +11:00
|
|
|
super().__init__()
|
2026-01-14 07:02:32 +11:00
|
|
|
self.zoom_level = 0.05
|
2026-01-14 12:20:53 +11:00
|
|
|
self.last_zoom_level = self.zoom_level
|
2026-01-14 09:11:09 +11:00
|
|
|
self.project = project
|
2026-01-15 07:58:09 +11:00
|
|
|
|
2026-01-18 07:04:54 +11:00
|
|
|
self.config_handler = ConfigHandler(self)
|
|
|
|
|
|
2026-01-15 16:24:43 +11:00
|
|
|
self.open_project_path = None
|
|
|
|
|
self.first_tab_click = True # stupid events firing when the app is first composed :/
|
|
|
|
|
|
2026-01-15 07:58:09 +11:00
|
|
|
@on(events.Key)
|
|
|
|
|
async def key_pressed(self, event: events.Key):
|
|
|
|
|
if event.key == "space":
|
|
|
|
|
timeline = self.query_one(Timeline)
|
|
|
|
|
if not timeline.song_player.paused:
|
|
|
|
|
timeline.song_player.pause()
|
|
|
|
|
else:
|
|
|
|
|
timeline.song_player.play_project(self.app.project)
|
2026-01-14 14:43:57 +11:00
|
|
|
|
2026-01-14 15:51:34 +11:00
|
|
|
def create_channel(self, name: str):
|
2026-01-15 16:24:43 +11:00
|
|
|
new_project_channel = ProjectChannel(
|
|
|
|
|
name
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-14 15:51:34 +11:00
|
|
|
self.query_one("#channels").mount(Channel(
|
|
|
|
|
len(self.project.channels),
|
2026-01-15 16:24:43 +11:00
|
|
|
new_project_channel,
|
2026-01-14 15:51:34 +11:00
|
|
|
name,
|
2026-01-15 16:24:43 +11:00
|
|
|
|
2026-01-14 15:51:34 +11:00
|
|
|
), before=-1)
|
2026-01-15 16:24:43 +11:00
|
|
|
self.query_one("#rows").mount(TimelineRow(new_project_channel))
|
2026-01-14 15:51:34 +11:00
|
|
|
|
2026-01-15 16:24:43 +11:00
|
|
|
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(
|
2026-01-16 08:26:15 +11:00
|
|
|
("TerminalDAW Project File", lambda p: p.suffix.lower() == ".tdp"),
|
|
|
|
|
("Any", lambda _: True)
|
|
|
|
|
|
2026-01-15 16:24:43 +11:00
|
|
|
)
|
|
|
|
|
), 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
|
2026-01-16 08:26:15 +11:00
|
|
|
rows.mount(row)
|
2026-01-15 16:24:43 +11:00
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.notify(f"Loaded \"{path}\"")
|
|
|
|
|
|
|
|
|
|
self.push_screen(FileOpen(
|
|
|
|
|
filters=Filters(
|
2026-01-16 08:26:15 +11:00
|
|
|
("TerminalDAW Project File", lambda p: p.suffix.lower() == ".tdp"),
|
|
|
|
|
("Any", lambda _: True)
|
|
|
|
|
|
2026-01-15 16:24:43 +11:00
|
|
|
)
|
|
|
|
|
), callback=callback)
|
|
|
|
|
|
2026-01-18 07:04:54 +11:00
|
|
|
case "Settings":
|
|
|
|
|
self.push_screen(SettingsScreen())
|
|
|
|
|
|
2026-01-15 16:24:43 +11:00
|
|
|
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)
|
2026-01-14 15:51:34 +11:00
|
|
|
|
2026-01-13 16:06:57 +11:00
|
|
|
def compose(self) -> ComposeResult:
|
2026-01-15 16:24:43 +11:00
|
|
|
yield Tabs(
|
|
|
|
|
Tab("File"),
|
|
|
|
|
Tab("Edit"),
|
|
|
|
|
Tab("About"),
|
|
|
|
|
id="top-menu")
|
2026-01-13 16:06:57 +11:00
|
|
|
yield Sidebar()
|
|
|
|
|
yield Timeline()
|
2026-01-13 21:33:25 +11:00
|
|
|
yield ProjectSettings()
|
2026-01-18 07:04:54 +11:00
|
|
|
yield Footer()
|
|
|
|
|
|
|
|
|
|
def on_mount(self):
|
|
|
|
|
# load config into the UI
|
|
|
|
|
self.config_handler.apply_settings()
|