diff --git a/docs/dev-workflow.md b/docs/dev-workflow.md index 723848a..4c2ac79 100644 --- a/docs/dev-workflow.md +++ b/docs/dev-workflow.md @@ -116,13 +116,19 @@ HTML-макеты → PNG: **headless Edge** (`--headless --screenshot`), он домена в конфиг; переключение галочек персистится - [x] URL-бар привязан к свойству (был захардкожен), метка чекбокса — домен -### E2 — страница настроек — ГОТОВО (базовая версия) +### E2 — страница настроек — ГОТОВО (отдельное окно) -- [x] Переключение вид/настройки по кнопке cog; Esc в настройках = назад; - env `BP_VIEW=settings` для скриншотов -- [x] Список браузеров: иконка + имя + путь (elide), удаление корзиной -- [x] Кнопка «Автодетект»: мерж найденных по пути, без дублей -- [x] Запомненные домены (domain → browser) с удалением +- [x] Настройки = **отдельное окно** `SettingsWindow` (760×560, обычная рамка, + resizable; в маленьком пикере редактировать неудобно). То же событийное + кольцо; Theme-глобал красит оба окна; модели шарятся (ModelRc один на оба) +- [x] cog в пикере → показать окно (центрирование через invoke_from_event_loop — + winit-окно реализуется лениво, как и у пикера) +- [x] Две панели: браузеры (иконка/имя/путь/корзина) + запомненные домены + (domain → browser/корзина); кнопка «Автодетект» (мерж по пути) +- [x] Esc в настройках = закрыть окно; X работает по умолчанию +- [x] env `BP_VIEW=settings` — запуск сразу с окном настроек (без пикера); + нюанс: `run()` авто-показывает своё окно, поэтому в этом режиме цикл + крутится на settings_window - [ ] Редактирование браузера (имя/путь/флаги через LineEdit) — следующий шаг - [ ] Чтение флагов из .lnk ярлыков (крейс `lnk`) — по желанию - [ ] Добавить логотипы Edge/Zen в logos/ (пересоздать набор из alrrr/browser-logos) diff --git a/src/main.rs b/src/main.rs index 391e260..70e5d24 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,9 +76,9 @@ fn init(cfg: Config, url: Option) -> State { refresh_models(&browser_model, &remembered_model, &cfg.borrow()); - if std::env::var("BP_VIEW").as_deref() == Ok("settings") { - main_window.set_settings_open(true); - } + let settings_window = SettingsWindow::new().unwrap(); + settings_window.set_browsers(browser_model.clone().into()); + settings_window.set_remembered(remembered_model.clone().into()); main_window.set_current_url( url.clone().unwrap_or_else(|| "ku6epxboctuk.github.io".into()).into(), @@ -202,24 +202,20 @@ fn init(cfg: Config, url: Option) -> State { }); main_window.on_settings_clicked({ - let main_window = main_window.clone_strong(); - let browser_model = browser_model.clone(); - let remembered_model = remembered_model.clone(); - let cfg = cfg.clone(); + let settings_window = settings_window.clone_strong(); move || { - refresh_models(&browser_model, &remembered_model, &cfg.borrow()); - main_window.set_settings_open(true); + settings_window.show().unwrap(); + let weak = settings_window.as_weak(); + let _ = slint::invoke_from_event_loop(move || { + if let Some(sw) = weak.upgrade() { + winit::center_window(sw.window()); + sw.invoke_restore_focus(); + } + }); } }); - main_window.on_settings_closed({ - let main_window = main_window.clone_strong(); - move || { - main_window.set_settings_open(false); - } - }); - - main_window.on_remove_browser({ + settings_window.on_remove_browser({ let browser_model = browser_model.clone(); let remembered_model = remembered_model.clone(); let cfg = cfg.clone(); @@ -232,7 +228,7 @@ fn init(cfg: Config, url: Option) -> State { } }); - main_window.on_remove_remembered({ + settings_window.on_remove_remembered({ let browser_model = browser_model.clone(); let remembered_model = remembered_model.clone(); let cfg = cfg.clone(); @@ -245,7 +241,7 @@ fn init(cfg: Config, url: Option) -> State { } }); - main_window.on_redetect_browsers({ + settings_window.on_redetect({ let browser_model = browser_model.clone(); let remembered_model = remembered_model.clone(); let cfg = cfg.clone(); @@ -268,21 +264,34 @@ fn init(cfg: Config, url: Option) -> State { } }); + settings_window.on_esc_pressed({ + let settings_window = settings_window.clone_strong(); + let main_window = main_window.clone_strong(); + move || { + let _ = settings_window.window().hide(); + main_window.invoke_restore_focus(); + } + }); + main_window.on_esc_pressed({ let main_window = main_window.clone_strong(); move || { - if main_window.get_settings_open() { - main_window.set_settings_open(false); - } else { - let _ = main_window.window().hide(); - } + let _ = main_window.window().hide(); + } + }); + + settings_window.window().on_close_requested({ + let main_window = main_window.clone_strong(); + move || { + main_window.invoke_restore_focus(); + slint::CloseRequestResponse::HideWindow } }); main_window.set_browser_model(browser_model.clone().into()); - main_window.set_remembered_list(remembered_model.clone().into()); State { main_window, + settings_window, browser_model, remembered_model, config: cfg, @@ -291,6 +300,7 @@ fn init(cfg: Config, url: Option) -> State { pub struct State { pub main_window: MainWindow, + pub settings_window: SettingsWindow, pub browser_model: Rc>, pub remembered_model: Rc>, pub config: Rc>, @@ -316,15 +326,29 @@ pub fn main() { } let state = init(cfg, url); - let main_window = state.main_window.clone_strong(); - main_window.show().unwrap(); - let weak = main_window.as_weak(); - let _ = slint::invoke_from_event_loop(move || { - if let Some(mw) = weak.upgrade() { - winit::center_window(mw.window()); - } - }); + if std::env::var("BP_VIEW").as_deref() == Ok("settings") { + state.settings_window.show().unwrap(); + let weak = state.settings_window.as_weak(); + let _ = slint::invoke_from_event_loop(move || { + if let Some(sw) = weak.upgrade() { + winit::center_window(sw.window()); + sw.invoke_restore_focus(); + } + }); + state.settings_window.run().unwrap(); + } else { + let main_window = state.main_window.clone_strong(); + main_window.show().unwrap(); - main_window.run().unwrap(); + let weak = main_window.as_weak(); + let _ = slint::invoke_from_event_loop(move || { + if let Some(mw) = weak.upgrade() { + winit::center_window(mw.window()); + mw.invoke_restore_focus(); + } + }); + + state.main_window.run().unwrap(); + } } diff --git a/ui/main.slint b/ui/main.slint index 5df4fde..b58c1ce 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -2,11 +2,12 @@ import { Button, CheckBox, ListView } from "std-widgets.slint"; import { BrowserItem } from "./browser_item.slint"; import { Header } from "./header.slint"; import { Footer } from "./footer.slint"; -import { SettingsView } from "./settings.slint"; -import { BrowserConfig, RememberedEntry } from "./types.slint"; +import { BrowserConfig } from "./types.slint"; import { Theme } from "theme.slint"; export { Theme } +export { SettingsWindow } from "./settings.slint"; +export { RememberedEntry } from "./types.slint"; component CheckRow inherits Rectangle { in property label; @@ -63,17 +64,11 @@ export component MainWindow inherits Window { in-out property remember-choice: false; in-out property always-ask: true; in property selected-index: 0; - in property <[RememberedEntry]> remembered-list: []; - in-out property settings-open: false; callback launch-browser(BrowserConfig); callback toggle-remember(bool); callback toggle-always-ask(bool); callback settings-clicked(); - callback settings-closed(); - callback remove-browser(string); - callback remove-remembered(string); - callback redetect-browsers(); callback open-clicked(); callback request-drag(); callback esc-pressed(); @@ -83,9 +78,10 @@ export component MainWindow inherits Window { width: 488px; height: 800px; default-font-family: "Segoe UI"; - - FocusScope { - init => { self.focus(); } + scope := FocusScope { + init => { + self.focus(); + } key-pressed(event) => { if (event.text == Key.Escape) { root.esc-pressed(); @@ -96,6 +92,10 @@ export component MainWindow inherits Window { } } + callback restore-focus(); + restore-focus => { + scope.focus(); + } VerticalLayout { alignment: start; width: 488px; @@ -113,24 +113,7 @@ export component MainWindow inherits Window { border-width: 1px; border-color: Theme.border-main; - if root.settings-open: SettingsView { - browsers: root.browser-model; - remembered: root.remembered-list; - back => { - root.settings-closed(); - } - remove-browser(name) => { - root.remove-browser(name); - } - remove-remembered(domain) => { - root.remove-remembered(domain); - } - redetect => { - root.redetect-browsers(); - } - } - - if !root.settings-open: VerticalLayout { + VerticalLayout { Header { url: current-url; diff --git a/ui/settings.slint b/ui/settings.slint index 1c28437..3684888 100644 --- a/ui/settings.slint +++ b/ui/settings.slint @@ -2,231 +2,233 @@ import { ListView } from "std-widgets.slint"; import { Theme } from "./theme.slint"; import { BrowserConfig, RememberedEntry } from "./types.slint"; -export component SettingsView inherits Rectangle { +export component SettingsWindow inherits Window { + title: "bropicker — настройки"; + width: 760px; + height: 560px; + background: Theme.surface-main; + default-font-family: "Segoe UI"; + in property <[BrowserConfig]> browsers; in property <[RememberedEntry]> remembered; - callback back(); callback remove-browser(string); callback remove-remembered(string); callback redetect(); + callback esc-pressed(); - background: transparent; + scope := FocusScope { + init => { + self.focus(); + } + key-pressed(event) => { + if (event.text == Key.Escape) { + root.esc-pressed(); + accept + } else { + reject + } + } + } + + callback restore-focus(); + restore-focus => { + scope.focus(); + } VerticalLayout { - padding: 16px; - spacing: 12px; + padding: 20px; + spacing: 14px; + + Text { + text: "Настройки"; + color: Theme.text-main; + font-size: 20px; + font-weight: 600; + } HorizontalLayout { - spacing: 8px; + spacing: 14px; - Rectangle { - width: 32px; - border-radius: 8px; - background: back-touch.has-hover ? Theme.surface-hover : transparent; + VerticalLayout { + spacing: 8px; - back-touch := TouchArea { - mouse-cursor: pointer; - clicked => { - root.back(); + HorizontalLayout { + spacing: 8px; + + Text { + text: "Браузеры"; + color: Theme.text-muted; + font-size: 13px; + vertical-alignment: center; + } + + Rectangle {} + + Rectangle { + width: 110px; + height: 26px; + border-radius: 8px; + background: redetect-touch.has-hover ? Theme.surface-hover : Theme.surface-elevated; + border-width: 1px; + border-color: Theme.border-main; + + redetect-touch := TouchArea { + mouse-cursor: pointer; + clicked => { + root.redetect(); + } + } + + Text { + width: 100%; + height: 100%; + text: "Автодетект"; + color: redetect-touch.has-hover ? Theme.text-main : Theme.text-control; + font-size: 12px; + horizontal-alignment: center; + vertical-alignment: center; + } } } - Image { - x: (parent.width - self.width) / 2; - y: (parent.height - self.height) / 2; - width: 20px; - height: 20px; - source: @image-url("../icons/arrow_left.svg"); - colorize: back-touch.has-hover ? Theme.text-main : Theme.text-muted; + Rectangle { + vertical-stretch: 1; + border-radius: 12px; + background: Theme.surface-elevated; + border-width: 1px; + border-color: Theme.border-main; + clip: true; + + ListView { + for b in root.browsers: Rectangle { + height: 56px; + background: transparent; + + HorizontalLayout { + padding-left: 14px; + padding-right: 10px; + spacing: 12px; + + Image { + width: 26px; + y: (parent.height - self.height) / 2; + source: b.icon; + } + + VerticalLayout { + horizontal-stretch: 1; + alignment: center; + + Text { + text: b.name; + color: Theme.text-main; + font-size: 14px; + } + + Text { + text: b.path; + color: Theme.text-hint; + font-size: 11px; + font-family: "Consolas"; + overflow: elide; + } + } + + Rectangle { + width: 30px; + border-radius: 6px; + background: del-touch.has-hover ? #ef444420 : transparent; + + del-touch := TouchArea { + mouse-cursor: pointer; + clicked => { + root.remove-browser(b.name); + } + } + + Image { + x: (parent.width - self.width) / 2; + y: (parent.height - self.height) / 2; + width: 16px; + height: 16px; + source: @image-url("../icons/trash.svg"); + colorize: del-touch.has-hover ? #ef4444 : Theme.text-muted; + } + } + } + } + } } } VerticalLayout { - alignment: center; + spacing: 8px; Text { - text: "Настройки"; - color: Theme.text-main; - font-size: 18px; - font-weight: 600; - vertical-alignment: center; - } - } - } - - HorizontalLayout { - spacing: 8px; - - Text { - text: "Браузеры"; - color: Theme.text-muted; - font-size: 13px; - vertical-alignment: center; - } - - Rectangle {} - - Rectangle { - width: 120px; - height: 26px; - border-radius: 8px; - background: redetect-touch.has-hover ? Theme.surface-hover : Theme.surface-elevated; - border-width: 1px; - border-color: Theme.border-main; - - redetect-touch := TouchArea { - mouse-cursor: pointer; - clicked => { - root.redetect(); - } + text: "Запомненные домены"; + color: Theme.text-muted; + font-size: 13px; } - Text { - width: 100%; - height: 100%; - text: "Автодетект"; - color: redetect-touch.has-hover ? Theme.text-main : Theme.text-control; - font-size: 12px; - horizontal-alignment: center; - vertical-alignment: center; - } - } - } + Rectangle { + vertical-stretch: 1; + border-radius: 12px; + background: Theme.surface-elevated; + border-width: 1px; + border-color: Theme.border-main; + clip: true; - Rectangle { - vertical-stretch: 3; - border-radius: 8px; - background: Theme.surface-elevated; - clip: true; - - ListView { - for b in root.browsers: Rectangle { - height: 52px; - background: transparent; - - HorizontalLayout { - padding-left: 12px; - padding-right: 8px; - spacing: 10px; - - Image { - width: 22px; - y: (parent.height - self.height) / 2; - source: b.icon; - } - - VerticalLayout { - horizontal-stretch: 1; - alignment: center; - - Text { - text: b.name; - color: Theme.text-main; - font-size: 14px; - } - - Text { - text: b.path; - color: Theme.text-hint; - font-size: 11px; - font-family: "Consolas"; - overflow: elide; - } - } - - Rectangle { - width: 28px; - border-radius: 6px; - background: del-touch.has-hover ? #ef444420 : transparent; - - del-touch := TouchArea { - mouse-cursor: pointer; - clicked => { - root.remove-browser(b.name); - } - } - - Image { - x: (parent.width - self.width) / 2; - y: (parent.height - self.height) / 2; - width: 15px; - height: 15px; - source: @image-url("../icons/trash.svg"); - colorize: del-touch.has-hover ? #ef4444 : Theme.text-muted; - } - } - } - } - } - } - - Text { - text: "Запомненные домены"; - color: Theme.text-muted; - font-size: 13px; - } - - Rectangle { - vertical-stretch: 2; - border-radius: 8px; - background: Theme.surface-elevated; - clip: true; - - ListView { - for r in root.remembered: Rectangle { - height: 40px; - background: transparent; - - HorizontalLayout { - padding-left: 12px; - padding-right: 8px; - spacing: 10px; - - VerticalLayout { - horizontal-stretch: 1; - alignment: center; + ListView { + for r in root.remembered: Rectangle { + height: 44px; + background: transparent; HorizontalLayout { - spacing: 6px; + padding-left: 14px; + padding-right: 10px; + spacing: 12px; - Text { - text: r.domain; - color: Theme.text-main; - font-size: 13px; - font-family: "Consolas"; - vertical-alignment: center; + VerticalLayout { + horizontal-stretch: 1; + alignment: center; + + Text { + text: r.domain; + color: Theme.text-main; + font-size: 14px; + font-family: "Consolas"; + } + + Text { + text: "→ " + r.browser; + color: Theme.text-hint; + font-size: 12px; + } } - Text { - text: "→ " + r.browser; - color: Theme.text-hint; - font-size: 12px; - vertical-alignment: center; + Rectangle { + width: 30px; + border-radius: 6px; + background: del2-touch.has-hover ? #ef444420 : transparent; + + del2-touch := TouchArea { + mouse-cursor: pointer; + clicked => { + root.remove-remembered(r.domain); + } + } + + Image { + x: (parent.width - self.width) / 2; + y: (parent.height - self.height) / 2; + width: 16px; + height: 16px; + source: @image-url("../icons/trash.svg"); + colorize: del2-touch.has-hover ? #ef4444 : Theme.text-muted; + } } } } - - Rectangle { - width: 28px; - border-radius: 6px; - background: del2-touch.has-hover ? #ef444420 : transparent; - - del2-touch := TouchArea { - mouse-cursor: pointer; - clicked => { - root.remove-remembered(r.domain); - } - } - - Image { - x: (parent.width - self.width) / 2; - y: (parent.height - self.height) / 2; - width: 15px; - height: 15px; - source: @image-url("../icons/trash.svg"); - colorize: del2-touch.has-hover ? #ef4444 : Theme.text-muted; - } - } } } }