working on openning and closing files
This commit is contained in:
@@ -2,6 +2,7 @@ pymp3
|
|||||||
textual
|
textual
|
||||||
textual-slider
|
textual-slider
|
||||||
textual-plot
|
textual-plot
|
||||||
|
textual-fspicker
|
||||||
numpy
|
numpy
|
||||||
msgpack-numpy
|
msgpack-numpy
|
||||||
pedalboard
|
pedalboard
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ if __name__ == "__main__":
|
|||||||
test_project.channels.append(piano_channel)
|
test_project.channels.append(piano_channel)
|
||||||
|
|
||||||
test_project.write_to_file("test_project.tdp")"""
|
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
|
# start the ui
|
||||||
print("Starting UI...")
|
print("Starting UI...")
|
||||||
|
|||||||
Binary file not shown.
146
src/ui/app.py
146
src/ui/app.py
@@ -2,12 +2,15 @@ from textual.app import App, ComposeResult
|
|||||||
from textual.widgets import Footer, Tab, Tabs, Header
|
from textual.widgets import Footer, Tab, Tabs, Header
|
||||||
from textual import on, events
|
from textual import on, events
|
||||||
|
|
||||||
|
from textual_fspicker import FileOpen, FileSave, Filters
|
||||||
|
|
||||||
from ui.widgets.sidebar import Sidebar
|
from ui.widgets.sidebar import Sidebar
|
||||||
from ui.widgets.timeline import Timeline, TimelineRow
|
from ui.widgets.timeline import Timeline, TimelineRow
|
||||||
from ui.widgets.project_settings import ProjectSettings
|
from ui.widgets.project_settings import ProjectSettings
|
||||||
from ui.widgets.channel import Channel
|
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):
|
class AppUI(App):
|
||||||
@@ -19,6 +22,9 @@ class AppUI(App):
|
|||||||
self.last_zoom_level = self.zoom_level
|
self.last_zoom_level = self.zoom_level
|
||||||
self.project = project
|
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)
|
@on(events.Key)
|
||||||
async def key_pressed(self, event: events.Key):
|
async def key_pressed(self, event: events.Key):
|
||||||
if event.key == "space":
|
if event.key == "space":
|
||||||
@@ -29,22 +35,140 @@ class AppUI(App):
|
|||||||
timeline.song_player.play_project(self.app.project)
|
timeline.song_player.play_project(self.app.project)
|
||||||
|
|
||||||
def create_channel(self, name: str):
|
def create_channel(self, name: str):
|
||||||
|
new_project_channel = ProjectChannel(
|
||||||
|
name
|
||||||
|
)
|
||||||
|
|
||||||
self.query_one("#channels").mount(Channel(
|
self.query_one("#channels").mount(Channel(
|
||||||
len(self.project.channels),
|
len(self.project.channels),
|
||||||
|
new_project_channel,
|
||||||
name,
|
name,
|
||||||
), before=-1)
|
|
||||||
self.query_one("#rows").mount(TimelineRow())
|
|
||||||
|
|
||||||
self.project.channels.append(ProjectChannel(
|
), before=-1)
|
||||||
name
|
self.query_one("#rows").mount(TimelineRow(new_project_channel))
|
||||||
))
|
|
||||||
|
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:
|
def compose(self) -> ComposeResult:
|
||||||
with Tabs(id="top-menu"):
|
yield Tabs(
|
||||||
yield Tab("File")
|
Tab("File"),
|
||||||
yield Tab("Edit")
|
Tab("Edit"),
|
||||||
yield Tab("View")
|
Tab("About"),
|
||||||
yield Tab("Help")
|
id="top-menu")
|
||||||
yield Sidebar()
|
yield Sidebar()
|
||||||
yield Timeline()
|
yield Timeline()
|
||||||
yield ProjectSettings()
|
yield ProjectSettings()
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from textual_slider import Slider
|
|||||||
|
|
||||||
from textual import on, events
|
from textual import on, events
|
||||||
|
|
||||||
|
from project import ProjectChannel
|
||||||
|
|
||||||
|
|
||||||
class Channel(VerticalGroup):
|
class Channel(VerticalGroup):
|
||||||
DEFAULT_CSS = """
|
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__()
|
super().__init__()
|
||||||
self.channel_index = channel_index
|
self.channel_index = channel_index
|
||||||
|
self.project_channel = project_channel
|
||||||
self.channel_name = channel_name
|
self.channel_name = channel_name
|
||||||
self.muted = muted
|
self.muted = muted
|
||||||
self.solo = solo
|
self.solo = solo
|
||||||
|
|||||||
119
src/ui/widgets/context_menu.py
Normal file
119
src/ui/widgets/context_menu.py
Normal 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
|
||||||
@@ -16,9 +16,12 @@ class ProjectSettings(Horizontal):
|
|||||||
align-vertical: middle;
|
align-vertical: middle;
|
||||||
padding: 0 1;
|
padding: 0 1;
|
||||||
|
|
||||||
Button {
|
#play-button {
|
||||||
max-width: 5;
|
max-width: 5;
|
||||||
}
|
}
|
||||||
|
#tempo-tap {
|
||||||
|
max-width: 7;
|
||||||
|
}
|
||||||
|
|
||||||
#song-bpm {
|
#song-bpm {
|
||||||
max-width: 12;
|
max-width: 12;
|
||||||
@@ -54,6 +57,7 @@ class ProjectSettings(Horizontal):
|
|||||||
|
|
||||||
yield Static(" BPM: ")
|
yield Static(" BPM: ")
|
||||||
yield Input("120", placeholder="120", valid_empty=False, type="integer", max_length=3, id="song-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 Static(" Time Signature: ")
|
||||||
yield Input("4/4", placeholder="4/4", valid_empty=False, id="song-time-sig")
|
yield Input("4/4", placeholder="4/4", valid_empty=False, id="song-time-sig")
|
||||||
@@ -27,6 +27,10 @@ class TimelineRow(Horizontal):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, project_channel):
|
||||||
|
super().__init__()
|
||||||
|
self.project_channel = project_channel
|
||||||
|
|
||||||
class Timeline(Vertical):
|
class Timeline(Vertical):
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
Timeline {
|
Timeline {
|
||||||
@@ -126,7 +130,7 @@ class Timeline(Vertical):
|
|||||||
|
|
||||||
with VerticalScroll(id="rows"):
|
with VerticalScroll(id="rows"):
|
||||||
for channel in self.app.project.channels:
|
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
|
row.styles.width = self.bar_offset * self.app.project.song_length
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
from textual.widgets import Tabs, Tab
|
|
||||||
Reference in New Issue
Block a user