Skip to content

Commit

Permalink
Add "Show/hide Playtime" toolbar button dynamically in REAPER dev ver…
Browse files Browse the repository at this point in the history
…sions
  • Loading branch information
helgoboss committed Mar 11, 2024
1 parent 6308c60 commit c1e18d6
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 27 deletions.
4 changes: 2 additions & 2 deletions dialogs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ mod mapping_panel;
mod mapping_row_panel;
mod mapping_rows_panel;
mod message_panel;
mod setup_panel;
mod shared_group_mapping_panel;
mod simple_editor_panel;
mod unit_panel;
mod welcome_panel;

pub fn generate_dialog_files(rc_dir: impl AsRef<Path>, bindings_file: impl AsRef<Path>) {
let default_font = Font {
Expand Down Expand Up @@ -155,7 +155,7 @@ pub fn generate_dialog_files(rc_dir: impl AsRef<Path>, bindings_file: impl AsRef
};
let simple_editor_panel_dialog = simple_editor_panel::create(context.global(), &mut ids);
let empty_panel_dialog = empty_panel::create(context.global(), &mut ids);
let setup_panel_dialog = setup_panel::create(context.global(), &mut ids);
let setup_panel_dialog = welcome_panel::create(context.global(), &mut ids);
let color_panel_dialog = color_panel::create(context.global(), &mut ids);
let resource = Resource {
dialogs: vec![
Expand Down
File renamed without changes.
7 changes: 5 additions & 2 deletions main/src/infrastructure/plugin/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ use reaper_high::{ActionKind, KeyBinding, KeyBindingKind, Reaper};
use reaper_medium::{AcceleratorBehavior, AcceleratorKeyCode};
use swell_ui::menu_tree::{item, menu, Entry};

pub const ACTION_SHOW_HIDE_PLAYTIME_COMMAND_NAME: &str = "HB_SHOW_HIDE_PLAYTIME";
pub const ACTION_SHOW_WELCOME_SCREEN_LABEL: &str = "Show welcome screen";

pub const ACTION_DEFS: &[ActionDef] = &[
ActionDef {
section: ActionSection::General,
command_name: "HB_SHOW_WELCOME_SCREEN",
action_name: "Show welcome screen",
action_name: ACTION_SHOW_WELCOME_SCREEN_LABEL,
op: BackboneShell::show_welcome_screen,
..DEFAULT_DEF
},
Expand Down Expand Up @@ -82,7 +85,7 @@ pub const ACTION_DEFS: &[ActionDef] = &[
},
ActionDef {
section: ActionSection::Playtime,
command_name: "HB_SHOW_HIDE_PLAYTIME",
command_name: ACTION_SHOW_HIDE_PLAYTIME_COMMAND_NAME,
action_name: "Show/hide Playtime",
op: BackboneShell::show_hide_playtime,
add_toolbar_button: true,
Expand Down
41 changes: 34 additions & 7 deletions main/src/infrastructure/plugin/backbone_shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ use crate::base::notification::notify_user_about_anyhow_error;
use crate::infrastructure::plugin::actions::ACTION_DEFS;
use crate::infrastructure::plugin::api_impl::{register_api, unregister_api};
use crate::infrastructure::plugin::debug_util::resolve_symbols_from_clipboard;
use crate::infrastructure::plugin::dynamic_toolbar::add_or_remove_toolbar_button;
use crate::infrastructure::plugin::persistent_toolbar::add_toolbar_button_persistently;
use crate::infrastructure::plugin::shutdown_detection_panel::ShutdownDetectionPanel;
use crate::infrastructure::plugin::toolbar::add_toolbar_button;
use crate::infrastructure::plugin::tracing_util::TracingHook;
use crate::infrastructure::plugin::{
ini_util, update_auto_units_async, SharedInstanceShell, WeakInstanceShell,
ACTION_SHOW_HIDE_PLAYTIME_COMMAND_NAME,
};
use crate::infrastructure::server::services::Services;
use crate::infrastructure::ui::instance_panel::InstancePanel;
Expand Down Expand Up @@ -82,7 +84,7 @@ use semver::Version;
use serde::{Deserialize, Serialize};
use slog::{debug, Drain};
use std::cell::{Ref, RefCell};
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::future::Future;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -439,6 +441,11 @@ impl BackboneShell {
// Must be called after registering actions and waking REAPER up, otherwise it won't find the command IDs.
let _ = Self::register_extension_menu();
let _ = Self::register_toolbar_icon_map();
for (key, value) in &config.toolbar {
if *value > 0 {
let _ = add_or_remove_toolbar_button(key, true);
}
}
// Detect shutdown via hidden child window as suggested by Justin
let shutdown_detection_panel = SharedView::new(ShutdownDetectionPanel::new());
shutdown_detection_panel.clone().open(reaper_window());
Expand Down Expand Up @@ -1047,6 +1054,19 @@ impl BackboneShell {
self.server.borrow_mut().stop();
}

/// Requires REAPER version >= 711+dev0305.
pub fn toggle_toolbar_button_dynamically(&self, command_name: &str) -> anyhow::Result<()> {
self.change_config(|config| {
// Adjust config
let value = config.toolbar.entry(command_name.to_string()).or_insert(0);
let enable = *value == 0;
*value = enable.into();
// Apply
add_or_remove_toolbar_button(command_name, enable)?;
Ok(())
})
}

/// Logging debug info is always initiated by a particular session.
pub fn log_debug_info(&self, session_id: &str) {
let msg = format!(
Expand All @@ -1070,11 +1090,12 @@ impl BackboneShell {
self.sessions_changed_subject.borrow().clone()
}

fn change_config(&self, op: impl FnOnce(&mut BackboneConfig)) {
fn change_config<R>(&self, op: impl FnOnce(&mut BackboneConfig) -> R) -> R {
let mut config = self.config.borrow_mut();
op(&mut config);
let result = op(&mut config);
config.save().unwrap();
self.notify_changed();
result
}

fn helgoboss_resource_dir_path() -> PathBuf {
Expand Down Expand Up @@ -1521,9 +1542,10 @@ impl BackboneShell {
}
}

pub fn add_toolbar_buttons() {
/// This is only necessary for REAPER versions < 7.11+dev0305
pub fn add_toolbar_buttons_persistently() {
for def in ACTION_DEFS.iter().filter(|def| def.add_toolbar_button) {
let result = add_toolbar_button(
let result = add_toolbar_button_persistently(
def.command_name,
&def.build_full_action_name(),
def.icon_file_name,
Expand Down Expand Up @@ -2135,6 +2157,7 @@ impl Drop for BackboneShell {
#[serde(default)]
pub struct BackboneConfig {
main: MainConfig,
toolbar: HashMap<String, u8>,
}

impl BackboneConfig {
Expand Down Expand Up @@ -2170,6 +2193,10 @@ impl BackboneConfig {
Url::parse(&self.main.companion_web_app_url).expect("invalid companion web app URL")
}

pub fn toolbar_button_is_enabled(&self, command_name: &str) -> bool {
self.toolbar.get(command_name).is_some_and(|v| *v != 0)
}

fn config_file_path() -> PathBuf {
BackboneShell::realearn_resource_dir_path().join("realearn.ini")
}
Expand Down Expand Up @@ -2848,7 +2875,7 @@ impl ToolbarIconMap for BackboneShell {
_toggle_state: Option<bool>,
) -> Option<&'static ReaperStr> {
Reaper::get().with_our_command(command_id, |command| match command?.command_name() {
"HB_SHOW_HIDE_PLAYTIME" => Some(reaper_str!("toolbar_playtime")),
ACTION_SHOW_HIDE_PLAYTIME_COMMAND_NAME => Some(reaper_str!("toolbar_playtime")),
_ => None,
})
}
Expand Down
76 changes: 76 additions & 0 deletions main/src/infrastructure/plugin/dynamic_toolbar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use crate::infrastructure::plugin::ACTION_SHOW_HIDE_PLAYTIME_COMMAND_NAME;
use anyhow::{bail, Context};
use reaper_high::Reaper;
use reaper_medium::{
CommandId, CommandItem, MenuOrToolbarItem, PositionDescriptor, UiRefreshBehavior,
};

/// Dynamically adds or removes a toolbar button without persisting it.
///
/// Requires REAPER version >= 711+dev0305.
///
/// # Errors
///
/// Returns and error if the command doesn't exist.
///
/// # Panics
///
/// Panics if the REAPER version is too low.
pub fn add_or_remove_toolbar_button(command_name: &str, add: bool) -> anyhow::Result<()> {
let action = Reaper::get().action_by_command_name(command_name);
let command_id = action.command_id()?;
let reaper = Reaper::get().medium_reaper();
match scan_toolbar_for_command_id(command_id) {
ToolbarScanOutcome::Exists { pos } => {
if !add {
reaper.delete_custom_menu_or_toolbar_item(
"Main toolbar",
pos,
UiRefreshBehavior::Refresh,
)?;
}
}
ToolbarScanOutcome::DoesntExist { .. } => {
if add {
reaper.add_custom_menu_or_toolbar_item_command(
"Main toolbar",
PositionDescriptor::Append,
command_id,
0,
action.name().unwrap_or_default(),
None,
UiRefreshBehavior::Refresh,
)?;
}
}
}
Ok(())
}

fn scan_toolbar_for_command_id(command_id: CommandId) -> ToolbarScanOutcome {
let reaper = Reaper::get().medium_reaper();
let mut i = 0;
loop {
let pos =
reaper.get_custom_menu_or_toolbar_item("Main toolbar", i, |result| match result? {
MenuOrToolbarItem::Command(item) if item.command_id == command_id => Some(Some(i)),
_ => Some(None),
});
match pos {
None => {
return ToolbarScanOutcome::DoesntExist {
toolbar_size: i + 1,
}
}
Some(None) => i += 1,
Some(Some(pos)) => {
return ToolbarScanOutcome::Exists { pos };
}
}
}
}

enum ToolbarScanOutcome {
Exists { pos: u32 },
DoesntExist { toolbar_size: u32 },
}
3 changes: 2 additions & 1 deletion main/src/infrastructure/plugin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ mod actions;
mod ini_util;
pub use actions::*;

mod dynamic_toolbar;
mod persistent_toolbar;
#[cfg(debug_assertions)]
mod sandbox;
mod shutdown_detection_panel;
mod toolbar;
mod unit_shell;
pub use unit_shell::*;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use anyhow::{bail, Context};
use reaper_high::Reaper;

pub fn add_toolbar_button(
/// This attempts to add a toolbar button persistently by modifying the "reaper-menu.ini" file.
///
/// This should only be used for REAPER versions < 711+dev0305. Later versions have API functions for dynamically
/// adding and removing toolbar buttons, which is more flexible and doesn't require a restart of REAPER.
pub fn add_toolbar_button_persistently(
command_name: &str,
action_label: &str,
icon_file_name: &str,
Expand Down
71 changes: 58 additions & 13 deletions main/src/infrastructure/ui/welcome_panel.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use enumset::EnumSet;
use reaper_high::Reaper;
use reaper_low::raw;
use std::fmt::Debug;

use crate::base::notification::alert;
use crate::infrastructure::plugin::BackboneShell;
use crate::infrastructure::plugin::{
BackboneShell, ACTION_SHOW_HIDE_PLAYTIME_COMMAND_NAME, ACTION_SHOW_WELCOME_SCREEN_LABEL,
};
use crate::infrastructure::ui::bindings::root;
use crate::infrastructure::ui::util::{fonts, symbols};
use swell_ui::{SharedView, View, ViewContext, Window};
Expand All @@ -19,6 +22,14 @@ impl WelcomePanel {
view: Default::default(),
}
}

fn toggle_toolbar_button(&self, command_name: &str) -> anyhow::Result<()> {
if custom_toolbar_api_is_available() {
BackboneShell::get().toggle_toolbar_button_dynamically(command_name)?;
}
self.invalidate_controls();
Ok(())
}
}

impl View for WelcomePanel {
Expand All @@ -32,8 +43,8 @@ impl View for WelcomePanel {

fn opened(self: SharedView<Self>, window: Window) -> bool {
window.center_on_screen();
let large_font = fonts::normal_font(window, 20);
let medium_font = fonts::normal_font(window, 14);
let large_font = fonts::normal_font(window, 18);
let medium_font = fonts::normal_font(window, 12);
// Text 1
let text_1 = window.require_control(root::ID_SETUP_INTRO_TEXT_1);
text_1.set_cached_font(large_font);
Expand All @@ -45,7 +56,7 @@ impl View for WelcomePanel {
// Text 3
let text_3 = window.require_control(root::ID_SETUP_TIP_TEXT);
let arrow = symbols::arrow_right_symbol();
text_3.set_text(format!("Tip: You can come back here at any time via\nExtensions {arrow} Helgobox {arrow} Show welcome screen"));
text_3.set_text(format!("Tip: You can come back here at any time via\nExtensions {arrow} Helgobox {arrow} {ACTION_SHOW_WELCOME_SCREEN_LABEL}"));
// Checkboxes
let playtime_checkbox = window.require_control(root::ID_SETUP_ADD_PLAYTIME_TOOLBAR_BUTTON);
playtime_checkbox.check();
Expand All @@ -56,10 +67,15 @@ impl View for WelcomePanel {
fn button_clicked(self: SharedView<Self>, resource_id: u32) {
match resource_id {
root::ID_SETUP_ADD_PLAYTIME_TOOLBAR_BUTTON => {
self.invalidate_controls();
self.toggle_toolbar_button(ACTION_SHOW_HIDE_PLAYTIME_COMMAND_NAME)
.expect("couldn't toggle toolbar button");
}
root::ID_SETUP_PANEL_OK => {
self.apply_and_close();
if custom_toolbar_api_is_available() {
self.close()
} else {
self.apply_and_close();
}
}
// IDCANCEL is escape button
raw::IDCANCEL => {
Expand All @@ -70,13 +86,37 @@ impl View for WelcomePanel {
}
}

fn custom_toolbar_api_is_available() -> bool {
Reaper::get()
.medium_reaper()
.low()
.pointers()
.GetCustomMenuOrToolbarItem
.is_some()
}

impl WelcomePanel {
fn invalidate_controls(&self) {
let button_text = if self.build_instructions().is_empty() {
"Close"
} else {
"Continue"
};
self.invalidate_playtime_checkbox();
self.invalidate_button();
}

fn invalidate_playtime_checkbox(&self) {
let checked = BackboneShell::get()
.config()
.toolbar_button_is_enabled(ACTION_SHOW_HIDE_PLAYTIME_COMMAND_NAME);
self.view
.require_control(root::ID_SETUP_ADD_PLAYTIME_TOOLBAR_BUTTON)
.set_checked(checked);
}

fn invalidate_button(&self) {
let button_text =
if self.build_instructions().is_empty() || custom_toolbar_api_is_available() {
"Close"
} else {
"Continue"
};
self.view
.require_control(root::ID_SETUP_PANEL_OK)
.set_text(button_text);
Expand All @@ -88,7 +128,12 @@ impl WelcomePanel {
for instruction in instructions {
instruction.execute();
}
alert("Additional setup finished!")
let addition = if custom_toolbar_api_is_available() {
""
} else {
"\n\nIf you enabled the toolbar button, please restart REAPER now (otherwise you will not see the button)!"
};
alert(format!("Additional setup finished!{addition}"));
}
self.close();
}
Expand All @@ -115,7 +160,7 @@ impl SetupInstruction {
pub fn execute(&self) {
match self {
SetupInstruction::PlaytimeToolbarButton => {
BackboneShell::add_toolbar_buttons();
BackboneShell::add_toolbar_buttons_persistently();
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion playtime-clip-engine

0 comments on commit c1e18d6

Please sign in to comment.