Skip to content

Commit

Permalink
refactor: switch to widget overlay; remove menu
Browse files Browse the repository at this point in the history
  • Loading branch information
dynobo committed Jul 27, 2024
1 parent b3bbf2f commit 886df65
Show file tree
Hide file tree
Showing 15 changed files with 534 additions and 347 deletions.
232 changes: 152 additions & 80 deletions myhumbleself/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
gi.require_version("Gdk", "4.0")
gi.require_version("Gtk", "4.0")
gi.require_version("GdkPixbuf", "2.0")
from gi.repository import Gdk, GdkPixbuf, Gio, Gtk # noqa: E402
from gi.repository import Gdk, GdkPixbuf, Gio, GObject, Gtk # noqa: E402

RESOURCE_PATH = Path(__file__).parent / "resources"

Expand All @@ -28,6 +28,28 @@


class MyHumbleSelf(Gtk.Application):
# Webcam widget
picture: Gtk.Picture

# Headerbar widgets
camera_dropdown: Gtk.DropDown
follow_face_button: Gtk.ToggleButton
shape_box: Gtk.FlowBox

# Controls Container
controls_grid: Gtk.Grid
overlay: Gtk.Overlay

# Controls
reset_button: Gtk.Button
toggle_controls_button: Gtk.Button
right_button: Gtk.Button
left_button: Gtk.Button
down_button: Gtk.Button
up_button: Gtk.Button
zoom_in_button: Gtk.Button
zoom_out_button: Gtk.Button

def __init__(self, application_id: str) -> None:
super().__init__(application_id=application_id)
self.win: Gtk.ApplicationWindow
Expand All @@ -42,114 +64,153 @@ def __init__(self, application_id: str) -> None:
self.fps: list[float] = [0]
self.fps_window = 50
self.face_coords = face_detection.Rect(0, 0, 1080, 1920)
self.cam_item_prefix = "■🢐 "

self.connect("activate", self.on_activate)
self.connect("shutdown", self.on_shutdown)

# TODO: split into smaller functions
def on_activate(self, app: Gtk.Application) -> None: # noqa: PLR0915
def on_activate(self, app: Gtk.Application) -> None:
self.resource = Gio.resource_load(
str(Path(__file__).parent / "resources" / "myhumbleself.gresource")
)
Gio.Resource._register(self.resource)
builder = Gtk.Builder()
builder.add_from_resource("/com/github/dynobo/myhumbleself/window.ui")

self.builder = Gtk.Builder()
self.builder.add_from_resource("/com/github/dynobo/myhumbleself/window.ui")

theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default())
theme.add_resource_path("/com/github/dynobo/myhumbleself/icons")

self.win = builder.get_object("main_window")
self.win = self.builder.get_object("main_window")
self.win.set_application(self)

self.picture = self.init_picture()

self.shape_box = self.init_shape_box()
self.follow_face_button = self.init_follow_face_button()
self.camera_dropdown = self.init_camera_dropdown()

self.controls_grid = self.builder.get_object("controls_grid")
self.overlay = self.builder.get_object("overlay")

self.toggle_controls_button = self.builder.get_object("toggle_controls_button")
self.toggle_controls_button.connect("clicked", self.on_toggle_controls_clicked)

self.reset_button = self.builder.get_object("reset_button")
self.reset_button.connect("clicked", self.on_reset_clicked)

self.zoom_in_button = self.builder.get_object("zoom_in_button")
self.zoom_in_button.connect("clicked", self.on_zoom_in)

self.zoom_out_button = self.builder.get_object("zoom_out_button")
self.zoom_out_button.connect("clicked", self.on_zoom_out)

self.left_button = self.builder.get_object("left_button")
self.left_button.connect("clicked", self.on_move_clicked, -1, 0)

self.right_button = self.builder.get_object("right_button")
self.right_button.connect("clicked", self.on_move_clicked, 1, 0)

self.up_button = self.builder.get_object("up_button")
self.up_button.connect("clicked", self.on_move_clicked, 0, -1)

self.down_button = self.builder.get_object("down_button")
self.down_button.connect("clicked", self.on_move_clicked, 0, 1)

self.init_css()

self.win.present()

def init_camera_dropdown(self) -> Gtk.DropDown:
last_cam_id = self.config["main"].getint("last_active_camera", 0)
last_cam_idx = 0
camera_list = Gtk.StringList()

for i, cam in enumerate(self.camera.available_cameras):
camera_list.append(f"{self.cam_item_prefix}{cam}")
if cam == last_cam_id:
last_cam_idx = i

# TODO: Remove hardcoded camera
camera_list.append(f"{self.cam_item_prefix}42")

camera_dropdown = self.builder.get_object("camera_dropdown")
camera_dropdown.props.model = camera_list
camera_dropdown.set_selected(last_cam_idx)
camera_dropdown.connect("notify::selected-item", self.on_camera_selected)
return camera_dropdown

def init_picture(self) -> Gtk.Picture:
# TODO: Make picture centered and click through transparent areas
# TODO: Show overlay on hover to indicate hide/show controls
# TODO: Show pointer cursor only on overlay
self.picture = builder.get_object("picture")
self.picture.add_tick_callback(self.draw_image)

evk = Gtk.GestureClick()
evk.connect("pressed", self.on_picture_clicked)
self.picture.add_controller(evk)
picture = self.builder.get_object("picture")
picture.add_tick_callback(self.draw_image)

evk2 = Gtk.EventControllerMotion()
evk2.connect("leave", self.on_picture_leave)
evk2.connect("motion", self.on_picture_enter)
self.picture.add_controller(evk2)

self.toggle_controls_button = builder.get_object("toggle_controls_button")
self.toggle_controls_button.set_cursor_from_name("pointer")
picture.add_controller(evk2)
return picture

self.titlebar = builder.get_object("titlebar")
def init_shape_box(self) -> Gtk.FlowBox:
shape_menu_button = self.builder.get_object("shape_menu_button")
shape_menu_button.set_icon_name("shapes-symbolic")

self.follow_face_button = builder.get_object("follow_face_button")
self.follow_face_button.set_icon_name("follow-face-symbolic")
self.follow_face_button.connect("clicked", self.on_follow_face_clicked)
self.follow_face_button.set_active(
self.config["main"].getboolean("follow_face")
)
shape_box = self.builder.get_object("shape_box")

self.css_provider = builder.get_object("css_provider")
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(),
self.css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
self.css_provider.load_from_resource(
"/com/github/dynobo/myhumbleself/style.css"
)

camera_list = Gtk.StringList()
for cam in self.camera.available_cameras:
camera_list.append(f"/dev/video{cam}")
# TODO: Remove
camera_list.append("/dev/video42")
self.camera_combobox = builder.get_object("camera_dropdown")
self.camera_combobox.props.model = camera_list

self.shape_box = builder.get_object("shape_box")
self.first_button = None
first_button = None
for shape in self.resource.enumerate_children(
"/com/github/dynobo/myhumbleself/shapes", Gio.ResourceLookupFlags.NONE
):
button = Gtk.ToggleButton()
button.set_size_request(32, 32)
button.set_size_request(56, 56)
button.set_icon_name(f"{shape[:-4]}-symbolic")
button.set_has_frame(False)
button.connect("toggled", self.on_shape_toggled, shape)
button.set_css_classes([*button.get_css_classes(), "shape-button"])

# Activate stored shape:
if shape == self.config["main"].get("shape"):
button.set_active(True)

if self.first_button is None:
self.first_button = button
# Set button group
if first_button is None:
first_button = button
else:
button.set_group(self.first_button)
self.shape_box.append(button)

self.center_button = builder.get_object("center_button")
self.center_button.connect("clicked", self.on_center_clicked)

self.left_button = builder.get_object("left_button")
self.left_button.connect("clicked", self.on_move_clicked, 1, 0)
button.set_group(first_button)

self.right_button = builder.get_object("right_button")
self.right_button.connect("clicked", self.on_move_clicked, -1, 0)
shape_box.append(button)

self.up_button = builder.get_object("up_button")
self.up_button.connect("clicked", self.on_move_clicked, 0, 1)
return shape_box

self.down_button = builder.get_object("down_button")
self.down_button.connect("clicked", self.on_move_clicked, 0, -1)
def init_follow_face_button(self) -> Gtk.ToggleButton:
follow_face_button = self.builder.get_object("follow_face_button")
follow_face_button.set_icon_name("follow-face-symbolic")
follow_face_button.connect("clicked", self.on_follow_face_clicked)
follow_face_button.set_active(self.config["main"].getboolean("follow_face"))
self.on_follow_face_clicked(follow_face_button)
return follow_face_button

self.zoom_scale = builder.get_object("zoom_scale")
self.zoom_scale.set_value(self.config["main"].getint("zoom_factor", 100))
self.zoom_scale.connect("value-changed", self.on_zoom_changed)

self.win.present()
def init_css(self) -> None:
self.css_provider = self.builder.get_object("css_provider")
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(),
self.css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
self.css_provider.load_from_resource(
"/com/github/dynobo/myhumbleself/style.css"
)

def on_follow_face_clicked(self, button: Gtk.ToggleButton) -> None:
self.config.set_persistent("follow_face", button.get_active())
if button.get_active():
button.set_tooltip_text("Do not follow face")
button.set_icon_name("follow-face-off-symbolic")
else:
button.set_tooltip_text("Follow face")
button.set_icon_name("follow-face-symbolic")

def on_shape_toggled(self, button: Gtk.ToggleButton, shape: str) -> None:
if not button.get_active():
Expand All @@ -168,9 +229,19 @@ def on_shape_toggled(self, button: Gtk.ToggleButton, shape: str) -> None:

self.config.set_persistent("shape", shape)

def on_center_clicked(self, button: Gtk.Button) -> None:
def on_camera_selected(
self, dropdown: Gtk.DropDown, selected_item: GObject.ParamSpec
) -> None:
selected_item = dropdown.get_selected_item().get_string()
cam_id = int(selected_item.removeprefix(self.cam_item_prefix))
self.camera.stop()
self.camera.start(cam_id)
self.config.set_persistent("last_active_camera", cam_id)

def on_reset_clicked(self, button: Gtk.Button) -> None:
self.config.set_persistent("offset_x", 0)
self.config.set_persistent("offset_y", 0)
self.config.set_persistent("zoom_factor", 100)

def on_move_clicked(self, button: Gtk.Button, factor_x: int, factor_y: int) -> None:
step_size = 20
Expand All @@ -183,8 +254,13 @@ def on_move_clicked(self, button: Gtk.Button, factor_x: int, factor_y: int) -> N
self.config["main"].getint("offset_y", 0) + factor_y * step_size,
)

def on_zoom_changed(self, scale: Gtk.Scale) -> None:
self.config.set_persistent("zoom_factor", int(scale.get_value()))
def on_zoom_in(self, btn: Gtk.Button) -> None:
zoom = self.config["main"].getint("zoom_factor", 100)
self.config.set_persistent("zoom_factor", int(zoom + 10 * 1))

def on_zoom_out(self, btn: Gtk.Button) -> None:
zoom = self.config["main"].getint("zoom_factor", 100)
self.config.set_persistent("zoom_factor", int(zoom + 10 * -1))

def on_shutdown(self, app: Gtk.Application) -> None:
self.camera.stop()
Expand Down Expand Up @@ -245,23 +321,19 @@ def draw_fps(self, image: np.ndarray) -> np.ndarray:
)
return image

def on_picture_clicked(
self, event: Gtk.GestureClick, n_press: int, x: float, y: float
) -> None:
double_clicks = 2
if n_press == double_clicks:
self.toggle_presentation_mode()
def on_toggle_controls_clicked(self, button: Gtk.Button) -> None:
self.toggle_presentation_mode()

def on_picture_enter(
self, event: Gtk.EventControllerMotion, x: float, y: float
) -> None:
self.toggle_controls_button.set_visible(True)
self.controls_grid.set_visible(True)

def on_picture_leave(
self,
event: Gtk.EventControllerMotion,
) -> None:
self.toggle_controls_button.set_visible(False)
self.controls_grid.set_visible(False)

def toggle_presentation_mode(self) -> None:
self.in_presentation_mode = not self.in_presentation_mode
Expand All @@ -270,11 +342,11 @@ def toggle_presentation_mode(self) -> None:
if self.in_presentation_mode:
css_classes.append("transparent")
self.win.set_decorated(False)
self.picture.set_margin_top(titlebar_height)
self.overlay.set_margin_top(titlebar_height)
else:
css_classes.remove("transparent")
self.win.set_decorated(True)
self.picture.set_margin_top(0)
self.overlay.set_margin_top(0)

self.win.set_css_classes(css_classes)

Expand All @@ -299,9 +371,9 @@ def draw_image(self, widget: Gtk.Widget, idle: Gdk.FrameClock) -> bool:
texture = Gdk.Texture.new_for_pixbuf(pixbuf)
widget.set_paintable(texture)

if not True and logger.getEffectiveLevel() == logging.DEBUG:
if logger.getEffectiveLevel() == logging.DEBUG:
self.win.set_title(
f"MyHumbleSelf -"
f"MyHumbleSelf - "
f"FPS in/out: {np.mean(self.camera.fps):.1f} / {np.mean(self.fps):.1f}"
)

Expand Down
Loading

0 comments on commit 886df65

Please sign in to comment.