Skip to content

Commit

Permalink
Merge branch 'refs/heads/ProgressSliderContainer' into ProgressSlider
Browse files Browse the repository at this point in the history
  • Loading branch information
pozitronik committed Nov 7, 2024
2 parents af1f233 + 7e556a1 commit f31c6b5
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 19 deletions.
12 changes: 6 additions & 6 deletions sinner/gui/GUIForm.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from psutil import WINDOWS

from sinner.gui.controls.FramePlayer.BaseFramePlayer import ROTATE_90_CLOCKWISE, ROTATE_180, ROTATE_90_COUNTERCLOCKWISE
from sinner.gui.controls.ProgressIndicator.SegmentedProgressBar import SegmentedProgressBar
from sinner.gui.controls.FramePosition.FrameSlider import FrameSlider
from sinner.models.Event import Event as SinnerEvent
from sinner.gui.GUIModel import GUIModel
from sinner.gui.controls.FramePosition.BaseFramePosition import BaseFramePosition
Expand Down Expand Up @@ -117,8 +117,7 @@ def __init__(self, parameters: Namespace):
self.NavigationFrame: Frame = Frame(self.GUIWindow) # it is a frame for navigation control and progressbar

self.StatusBar = StatusBar(self.GUIWindow, borderwidth=1, relief=RIDGE, items={"Target resolution": "", "Render size": ""})
self.ProcessingProgress = SegmentedProgressBar(self.NavigationFrame, colors={0: 'black', 1: 'yellow', 2: 'green', 3: 'red'})
self.GUIModel = GUIModel(parameters, pb_control=self.ProcessingProgress, status_callback=lambda name, value: self.StatusBar.item(name, value), on_close_event=self._event_player_window_closed)
self.GUIModel = GUIModel(parameters, status_callback=lambda name, value: self.StatusBar.item(name, value), on_close_event=self._event_player_window_closed)

def on_player_window_close() -> None:
self.GUIModel.player_stop(wait=True)
Expand Down Expand Up @@ -152,7 +151,7 @@ def on_player_window_key_release(event: Event) -> None: # type: ignore[type-arg
on_self_run_button_press()

# Navigation slider
self.NavigateSlider: BaseFramePosition = SliderFramePosition(self.NavigationFrame, from_=1, variable=self.GUIModel.position, command=lambda position: self.GUIModel.rewind(int(position)))
self.NavigateSlider: BaseFramePosition = FrameSlider(self.NavigationFrame, from_=1, variable=self.GUIModel.position, command=lambda position: self.GUIModel.rewind(int(position)))

# Controls frame and contents
self.BaseFrame: Frame = Frame(self.GUIWindow) # it is a frame that holds all static controls with fixed size, such as main buttons and selectors
Expand Down Expand Up @@ -279,11 +278,12 @@ def switch_audio_backend(backend: str) -> None:

# maintain the order of window controls
def draw_controls(self) -> None:
# self.NavigateSlider.pack(anchor=CENTER, side=TOP, expand=False, fill=X)
self.NavigationFrame.pack(fill=X, expand=False, anchor=NW)
self.NavigateSlider.pack(anchor=NW, side=LEFT, expand=True, fill=BOTH)
self.ProcessingProgress.pack(anchor=NW, side=LEFT, expand=True, fill=X)
self.update_slider_bounds()

self.GUIModel.progress_control = self.NavigateSlider.progress

self.RunButton.pack(side=TOP, fill=BOTH, expand=True)
self.ButtonsFrame.pack(anchor=CENTER, expand=False, side=LEFT, fill=BOTH)
self.BaseFrame.pack(anchor=NW, expand=False, side=TOP, fill=X)
Expand Down
37 changes: 25 additions & 12 deletions sinner/gui/GUIModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class GUIModel(AttributeLoader, StatusMixin):
# internal/external objects
TimeLine: FrameTimeLine
Player: BaseFramePlayer
ProgressBar: BaseProgressIndicator
_ProgressBar: BaseProgressIndicator | None = None
AudioPlayer: BaseAudioBackend | None = None

_processors: dict[str, BaseFrameProcessor] # cached processors for gui [processor_name, processor]
Expand Down Expand Up @@ -147,7 +147,7 @@ def rules(self) -> Rules:
}
]

def __init__(self, parameters: Namespace, pb_control: BaseProgressIndicator, status_callback: Callable[[str, str], Any], on_close_event: Event | None = None):
def __init__(self, parameters: Namespace, status_callback: Callable[[str, str], Any], on_close_event: Event | None = None, progress_control: BaseProgressIndicator | None = None):
self.parameters = parameters
super().__init__(parameters)
self._processors = {}
Expand All @@ -159,9 +159,8 @@ def __init__(self, parameters: Namespace, pb_control: BaseProgressIndicator, sta

if self._enable_sound:
self.AudioPlayer = BaseAudioBackend.create(self._audio_backend, parameters=self.parameters, media_path=self._target_path)
self.ProgressBar = pb_control
self.ProgressBar.set_segments(self.frame_handler.fc)
self.ProgressBar.set_segment_values(self.TimeLine.processed_frames, PROCESSED)

self.progress_control = progress_control
self._status = status_callback
self._status("Time position", seconds_to_hmsms(0))

Expand Down Expand Up @@ -204,8 +203,7 @@ def source_path(self, value: str | None) -> None:
self.parameters.source = value
self.reload_parameters()
self.TimeLine = FrameTimeLine(source_name=self._source_path, target_name=self._target_path, temp_dir=self.temp_dir, frame_time=self.frame_handler.frame_time, start_frame=self.TimeLine.last_requested_index, end_frame=self.frame_handler.fc)
self.ProgressBar.set_segments(self.frame_handler.fc)
self.ProgressBar.set_segment_values(self.TimeLine.processed_frames, PROCESSED)
self.progress_control = self._ProgressBar # to update segments
if not self.player_is_started:
self.update_preview()

Expand All @@ -219,8 +217,7 @@ def target_path(self, value: str | None) -> None:
self.reload_parameters()
self.Player.clear()
self.TimeLine = FrameTimeLine(source_name=self._source_path, target_name=self._target_path, temp_dir=self.temp_dir, frame_time=self.frame_handler.frame_time, start_frame=1, end_frame=self.frame_handler.fc)
self.ProgressBar.set_segments(self.frame_handler.fc)
self.ProgressBar.set_segment_values(self.TimeLine.processed_frames, PROCESSED)
self.progress_control = self._ProgressBar # to update segments
if self._enable_sound:
self.AudioPlayer = BaseAudioBackend.create(self._audio_backend, parameters=self.parameters, media_path=self._target_path)
if self.player_is_started:
Expand Down Expand Up @@ -292,7 +289,7 @@ def update_preview(self, processed: bool | None = None) -> None:

if preview_frame:
self.Player.show_frame(preview_frame.frame)
self.ProgressBar.set_segment_value(self.position.get(), PROCESSED if processed else EXTRACTED)
self.set_progress_index_value(self.position.get(), PROCESSED if processed else EXTRACTED)
else:
self.Player.clear()

Expand Down Expand Up @@ -395,7 +392,7 @@ def process_done(future_: Future[tuple[float, int] | None]) -> None:
process_time, frame_index = result
self._average_processing_time.update(process_time / self.execution_threads)
processing.remove(frame_index)
self.ProgressBar.set_segment_value(frame_index, PROCESSED)
self.set_progress_index_value(frame_index, PROCESSED)
self._processing_fps = 1 / self._average_processing_time.get_average()
if self._biggest_processed_frame < frame_index:
self._biggest_processed_frame = frame_index
Expand All @@ -417,7 +414,7 @@ def process_done(future_: Future[tuple[float, int] | None]) -> None:
future: Future[tuple[float, int] | None] = executor.submit(self._process_frame, next_frame)
future.add_done_callback(process_done)
futures.append(future)
self.ProgressBar.set_segment_value(next_frame, PROCESSING)
self.set_progress_index_value(next_frame, PROCESSING)
if len(futures) >= self.execution_threads:
futures[:1][0].result()

Expand Down Expand Up @@ -480,6 +477,7 @@ def _show_frames(self) -> None:
self.position.set(self.TimeLine.last_returned_index)
if self.TimeLine.last_returned_index:
self._status("Time position", seconds_to_hmsms(self.TimeLine.last_returned_index * self.frame_handler.frame_time))
self._status("Frame position", f'{self.position.get()}/{self.frame_handler.fc}')
loop_time = time.perf_counter() - start_time # time for the current loop, sec
sleep_time = self.frame_handler.frame_time - loop_time # time to wait for the next loop, sec
if sleep_time > 0:
Expand Down Expand Up @@ -509,3 +507,18 @@ def get_mem_usage() -> str:
mem_rss = get_mem_usage()
mem_vms = get_mem_usage('vms')
return '{:.2f}'.format(mem_rss).zfill(5) + '/' + '{:.2f}'.format(mem_vms).zfill(5) + ' MB'

def set_progress_index_value(self, index: int, value: int) -> None:
if self._ProgressBar:
self._ProgressBar.set_segment_value(index, value)

@property
def progress_control(self) -> BaseProgressIndicator | None:
return self._ProgressBar

@progress_control.setter
def progress_control(self, value: BaseProgressIndicator | None) -> None:
self._ProgressBar = value
if self._ProgressBar:
self._ProgressBar.set_segments(self.frame_handler.fc + 1) # todo: разобраться, почему прогрессбар требует этот один лишний индекс
self._ProgressBar.set_segment_values(self.TimeLine.processed_frames, PROCESSED)
102 changes: 102 additions & 0 deletions sinner/gui/controls/FramePosition/FrameSlider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from tkinter import DISABLED, NORMAL, IntVar
from typing import Any

from customtkinter import CTkSlider

from sinner.gui.controls.FramePosition.BaseFramePosition import BaseFramePosition
from sinner.gui.controls.ProgressIndicator.SegmentedProgressBar import SegmentedProgressBar


class FrameSlider(CTkSlider, BaseFramePosition):

def __init__(self, master: Any, **kwargs):
progress_height = 10

# Инициализируем базовый слайдер с измененными параметрами
CTkSlider.__init__(
self,
master=master,
height=20,
button_corner_radius=1,
# button_length=2, # делаем ползунок более узким,
corner_radius=0,
border_width=0,
**kwargs
)

self.progress = SegmentedProgressBar(
self.master,
height=progress_height,
borderwidth=0,
border=0,
colors={0: 'orange', 1: 'yellow', 2: 'green', 3: 'red'}
)

self.progress.place(
in_=self,
x=0,
y=self.winfo_reqheight() - progress_height,
relwidth=1.0, # занимает всю ширину
height=progress_height
)

self.progress.pass_through = self

def pack(self, **kwargs) -> Any:
result = CTkSlider.pack(self, **kwargs)
return result

def pack_forget(self) -> Any:
self.pack_forget()

def _clicked(self, event: Any | None = None) -> None:
CTkSlider._clicked(self, event)

def set(self, output_value: int, from_variable_callback: bool = False) -> None:
if self._from_ < self._to:
if output_value > self._to:
output_value = self._to
elif output_value < self._from_:
output_value = self._from_
else:
if output_value < self._to:
output_value = self._to
elif output_value > self._from_:
output_value = self._from_

self._output_value = self._round_to_step_size(output_value)
try:
self._value = (self._output_value - self._from_) / (self._to - self._from_)
except ZeroDivisionError:
self._value = 1

self._draw()

if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(round(self._output_value) if isinstance(self._variable, IntVar) else self._output_value)
self._variable_callback_blocked = False

@property
def to(self) -> int:
return self._to

@to.setter
def to(self, value: int) -> None:
if value > self.position:
self.position = value
self.configure(to=value)

@property
def position(self) -> int:
return int(self.get())

@position.setter
def position(self, value: int) -> None:
self.set(value)

def disable(self) -> None:
CTkSlider.configure(self, True, state=DISABLED)

def enable(self) -> None:
CTkSlider.configure(self, True, state=NORMAL)
65 changes: 64 additions & 1 deletion sinner/gui/controls/ProgressIndicator/SegmentedProgressBar.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class UpdateCommand:


class SegmentedProgressBar(BaseProgressIndicator, tk.Canvas):
_pass_through: tk.Widget | None = None

def __init__(self, master: tk.Misc | None, segments: int = 100, width: int = 0, height: int = 10, min_visible_width: int = 0, colors: Dict[int, str] | None = None, **kwargs): # type: ignore[no-untyped-def]
"""
Создает сегментированный прогресс-бар
Expand All @@ -33,6 +35,8 @@ def __init__(self, master: tk.Misc | None, segments: int = 100, width: int = 0,
min_visible_width: минимальная видимая ширина группы сегментов в пикселях
colors: словарь соответствия значений цветам (например {0: 'white', 1: 'blue'})
"""
kwargs['highlightthickness'] = 0 # Убирает внешнюю рамку фокуса
kwargs['bd'] = 0 # Убирает бордюр
super().__init__(master, width=width, height=height, **kwargs)

self.states: List[int] = []
Expand Down Expand Up @@ -108,7 +112,7 @@ def update_size(self) -> None:

# Создаем фон
self.create_rectangle(0, 0, self.width, self.height,
fill='white', outline='gray',
fill='blue', outline='red',
tags="background")

# Создаем сегменты заново с новыми размерами
Expand Down Expand Up @@ -244,3 +248,62 @@ def _redraw(self) -> None:
segment_id = self.segment_ids[i]
self.itemconfig(segment_id,
fill=self.colors.get(value, DEFAULT_SEGMENT_COLOR))

@property
def pass_through(self) -> tk.Widget | None:
return self._pass_through

@pass_through.setter
def pass_through(self, value: tk.Widget | None) -> None:
self._pass_through = value
if self._pass_through:
# Привязываем события мыши
self.bind('<Button-1>', self._handle_mouse_event)
self.bind('<ButtonRelease-1>', self._handle_mouse_event)
self.bind('<Button-2>', self._handle_mouse_event)
self.bind('<ButtonRelease-2>', self._handle_mouse_event)
self.bind('<Button-3>', self._handle_mouse_event)
self.bind('<ButtonRelease-3>', self._handle_mouse_event)
self.bind('<Motion>', self._handle_mouse_event)
self.bind('<B1-Motion>', self._handle_mouse_event)
self.bind('<B2-Motion>', self._handle_mouse_event)
self.bind('<B3-Motion>', self._handle_mouse_event)
self.bind('<Enter>', self._handle_mouse_event)
self.bind('<Leave>', self._handle_mouse_event)

def _handle_mouse_event(self, event: tk.Event) -> str: # type: ignore[type-arg]
"""
Транслирует событие мыши в CTkSlider
Изначально код задумывался для трансляции любых событий в любой виджет, но
оказалось, что слайдер имеет кастомную обработку, и пришлось это учесть.
"""
if not self.pass_through or not hasattr(self._pass_through, '_clicked'):
return ""

# Конвертируем координаты
abs_x = self.winfo_rootx() + event.x
abs_y = self.winfo_rooty() + event.y
rel_x = abs_x - self._pass_through.winfo_rootx()
rel_y = abs_y - self._pass_through.winfo_rooty()

# Создаем новое событие
new_event = tk.Event()
new_event.x = rel_x
new_event.y = rel_y
new_event.x_root = event.x_root
new_event.y_root = event.y_root
new_event.type = event.type
new_event.widget = self._pass_through

# Словарь соответствия событий методам слайдера
handlers = {
tk.EventType.ButtonPress: '_clicked',
tk.EventType.Motion: '_motion',
tk.EventType.ButtonRelease: '_released'
}

# Вызываем соответствующий метод, если он есть
if event.type in handlers and hasattr(self._pass_through, handlers[event.type]):
getattr(self._pass_through, handlers[event.type])(new_event)

return "break"

0 comments on commit f31c6b5

Please sign in to comment.