diff --git a/data/re.sonny.Junction.gschema.xml b/data/re.sonny.Junction.gschema.xml index c09356e..fb27ba8 100644 --- a/data/re.sonny.Junction.gschema.xml +++ b/data/re.sonny.Junction.gschema.xml @@ -1,8 +1,21 @@ + + + + + + false + + false + + + 'default' + diff --git a/src/AppButton.blp b/src/AppButton.blp index f4cd78d..46e81d3 100644 --- a/src/AppButton.blp +++ b/src/AppButton.blp @@ -10,22 +10,34 @@ Button button { styles ["flat"] - Box box { - orientation: vertical; - valign: center; - halign: center; + Overlay { + Box box { + orientation: vertical; + valign: center; + halign: center; - Image image { - pixel-size: 92; - width-request: 92; - height-request: 92; - styles ["icon-dropshadow"] + Image image { + pixel-size: 92; + width-request: 92; + height-request: 92; + styles ["icon-dropshadow"] + } + + Label label { + ellipsize: end; + max-width-chars: 10; + margin-top: 6; + } } - Label label { - ellipsize: end; - max-width-chars: 10; - margin-top: 6; + // the 1...9 key that opens this app, set in AppButton.js + [overlay] + Label position { + halign: start; + valign: start; + visible: false; + can-target: false; + styles ["position"] } } } diff --git a/src/AppButton.js b/src/AppButton.js index 2837bd7..6431102 100644 --- a/src/AppButton.js +++ b/src/AppButton.js @@ -15,36 +15,105 @@ import Interface from "./AppButton.blp" with { type: "uri" }; const portal = new Xdp.Portal(); Gio._promisify(portal, "open_directory", "open_directory_finish"); +const ICON_SIZE = 92; +const ICON_SIZE_COMPACT = 64; +const TILE_SIZE = 134; +// icon + 8px of breathing room on each side, so the tile hugs the icon +const TILE_SIZE_COMPACT = 80; +// only 1...9 can be typed, see getButtonForKeyval in window.js +const MAX_POSITION = 9; + +// Show which of the 1...9 keys opens this app. Without it the shortcut is +// only discoverable from the Keyboard Shortcuts window. +function setPosition(label, position) { + if (!position || position > MAX_POSITION) return; + label.label = String(position); + label.visible = true; +} + +// GtkImage:pixel-size and size requests cannot be set from CSS, so the +// compact sizes are applied here rather than in style.css. +function bindCompact({ + button, + image, + label, + icon_size = ICON_SIZE, + icon_size_compact = ICON_SIZE_COMPACT, +}) { + function update() { + const compact = settings.get_boolean("compact"); + + const size = compact ? icon_size_compact : icon_size; + image.set_pixel_size(size); + image.set_size_request(size, size); + + const tile = compact ? TILE_SIZE_COMPACT : TILE_SIZE; + button.set_size_request(tile, tile); + + // A name doesn't fit under a compact tile - it ends up ellipsized to + // uselessness - so compact mode is icons only, whatever "Show App Names" + // is set to. The name is still in the tooltip. + label.visible = !compact && settings.get_boolean("show-app-names"); + } + + const handlers = [ + settings.connect("changed::compact", update), + settings.connect("changed::show-app-names", update), + ]; + button.connect("destroy", () => { + for (const handler of handlers) settings.disconnect(handler); + }); + update(); +} + export function TileButton({ label, tooltip = label, icon_name, icon_size, + icon_size_compact, + position, onClicked, }) { const { button, label: glabel, image, + position: position_label, } = build(Interface, { onClicked, }); button.set_tooltip_text(tooltip); glabel.label = label; - glabel.visible = false; - settings.bind("show-app-names", glabel, "visible", Gio.SettingsBindFlags.GET); + setPosition(position_label, position); image.set_from_icon_name(icon_name); - if (icon_size) { - image.set_pixel_size(icon_size); - } + bindCompact({ + button, + image, + label: glabel, + icon_size, + icon_size_compact, + }); return button; } -export default function AppButton({ appInfo, content_type, entry, window }) { - const { button, label, image, box } = build(Interface, { +export default function AppButton({ + appInfo, + content_type, + entry, + window, + position, +}) { + const { + button, + label, + image, + box, + position: position_label, + } = build(Interface, { onClicked() { open(true); }, @@ -53,8 +122,7 @@ export default function AppButton({ appInfo, content_type, entry, window }) { const name = appInfo.get_display_name(); button.set_tooltip_text(name); label.label = name; - label.visible = false; - settings.bind("show-app-names", label, "visible", Gio.SettingsBindFlags.GET); + setPosition(position_label, position); const menu = new Gio.Menu(); const popoverMenu = Gtk.PopoverMenu.new_from_model(menu); @@ -67,6 +135,8 @@ export default function AppButton({ appInfo, content_type, entry, window }) { image.set_from_file(getIconFilename(icon.get_file().get_path())); } + bindCompact({ button, image, label }); + function open(close_on_success) { const success = openWithApplication({ appInfo, @@ -150,7 +220,7 @@ export default function AppButton({ appInfo, content_type, entry, window }) { return button; } -export function ShowInFolderButton({ file, window }) { +export function ShowInFolderButton({ file, window, position }) { function onClicked() { portal .open_directory( @@ -168,6 +238,8 @@ export function ShowInFolderButton({ file, window }) { tooltip: _("View File in File Manager"), icon_name: "folder-symbolic", icon_size: 48, + icon_size_compact: 32, + position, onClicked, }); } diff --git a/src/application.js b/src/application.js index 4dd368b..c1ce7de 100644 --- a/src/application.js +++ b/src/application.js @@ -8,6 +8,7 @@ import Window from "./window.js"; import Welcome from "./welcome.js"; import About from "./about.js"; import ShortcutsWindow from "./ShortcutsWindow.js"; +import { settings } from "./common.js"; import "./style.css"; @@ -46,7 +47,15 @@ export default function Application() { } application.connect("startup", () => { - Adw.StyleManager.get_default().set_color_scheme(Adw.ColorScheme.FORCE_DARK); + // "default" lets the desktop decide - the color scheme, accent color and + // contrast preference are read from the org.freedesktop.appearance portal, + // which GNOME, KDE Plasma and others implement. + const style_manager = Adw.StyleManager.get_default(); + function updateColorScheme() { + style_manager.set_color_scheme(settings.get_enum("color-scheme")); + } + settings.connect("changed::color-scheme", updateColorScheme); + updateColorScheme(); }); // FIXME: Cannot deal with mailto:, xmpp:, ... URIs @@ -124,6 +133,8 @@ export default function Application() { application.add_action(quit); application.set_accels_for_action("app.quit", ["Q"]); + application.add_action(settings.create_action("color-scheme")); + application.set_accels_for_action("window.close", ["W", "Escape"]); application.set_accels_for_action("win.copy", ["C"]); diff --git a/src/style.css b/src/style.css index 19612a1..0e4c257 100644 --- a/src/style.css +++ b/src/style.css @@ -2,14 +2,13 @@ padding: 68px; } -.welcome button.link { - color: white; -} - .main { border-radius: 30px; background: none; - background-color: #353433; + /* Junction floats above the desktop like a popover rather than sitting on + it like a document window, so it follows the popover background - which + is also a touch lighter than the window background in dark styles. */ + background-color: @popover_bg_color; border: none; box-shadow: none; } @@ -59,11 +58,17 @@ font-size: 0.6rem; } +.main .list button:hover { + background-color: alpha(currentColor, 0.08); +} + +/* Tiles are reachable with the 1-9 keys, so the focused tile has to be + obvious. Tint it with the desktop accent color instead of a fixed white + wash, which was invisible on light backgrounds. */ .main .list button:focus { outline: none; - /* This is libadwaita background for active */ - /* https://gitlab.gnome.org/GNOME/libadwaita/-/blob/main/src/stylesheet/widgets/_buttons.scss#L3 */ - background-color: alpha(white, 0.3); + background-color: alpha(@accent_bg_color, 0.25); + box-shadow: inset 0 0 0 2px @accent_color; } .main .list button label { @@ -71,8 +76,79 @@ font-weight: 400; } +/* The 1...9 key that opens each app */ +.main .list button .position { + font-size: 11px; + font-weight: 700; + min-width: 18px; + min-height: 18px; + padding: 0 4px; + margin: 4px; + border-radius: 9px; + background-color: alpha(currentColor, 0.12); +} + +.main .list button:focus .position { + background-color: @accent_bg_color; + color: @accent_fg_color; +} + .main entry.uri { - background-color: #1e1e1e; + background-color: @view_bg_color; border-radius: 18px; - border: solid 1px black; + border: solid 1px @borders; + box-shadow: none; +} + +/* Compact mode - smaller icons, tiles and padding so the window takes up + much less room. */ + +/* 30px is a big bite out of a 140px tall window - the corner curves into + the first tile and pushes it inwards. */ +.main.compact { + border-radius: 18px; +} + +.main.compact .list { + padding-top: 6px; + padding-left: 6px; + padding-right: 6px; +} + +.main.compact .bar { + padding: 6px; +} + +.main.compact .list button { + border-radius: 12px; + padding: 6px; +} + +.main.compact .list button label { + font-size: 12px; +} + +.main.compact .list button .position { + font-size: 9px; + min-width: 14px; + min-height: 14px; + padding: 0 3px; + margin: 1px; + border-radius: 7px; +} + +.main.compact entry.uri { + border-radius: 12px; +} + +/* High contrast: the accent wash alone is too subtle, so lean on the + border and a solid focus ring. */ +@media (prefers-contrast: more) { + .main entry.uri { + border-width: 2px; + } + + .main .list button:focus { + box-shadow: inset 0 0 0 3px @accent_color; + } } diff --git a/src/window.blp b/src/window.blp index b6d4d1e..ac7795b 100644 --- a/src/window.blp +++ b/src/window.blp @@ -2,10 +2,7 @@ using Gtk 4.0; using Adw 1; Adw.ApplicationWindow window { - default-width: 772; - default-height: 218; - width-request: 360; - height-request: 218; + // the size is set in window.js, it depends on the "compact" setting styles ["main"] // portrait @@ -27,7 +24,7 @@ Adw.ApplicationWindow window { Adw.HeaderBar header_bar { [start] MenuButton { - popover: menu_popover; + menu-model: menu_model; icon-name: "open-menu-symbolic"; tooltip-text: _("Main Menu"); valign: center; @@ -60,7 +57,7 @@ Adw.ApplicationWindow window { styles ["bar"] MenuButton button_menu_bottom { - popover: menu_popover; + menu-model: menu_model; icon-name: "open-menu-symbolic"; tooltip-text: _("Main Menu"); valign: center; @@ -82,22 +79,44 @@ Adw.ApplicationWindow window { }; } -PopoverMenu menu_popover { - menu-model: menu_model; - halign: start; -} - menu menu_model { item { label: _("Show App Names"); action: "win.show-app-names"; } + item { + label: _("Compact Mode"); + action: "win.compact"; + } + item { label: _("Copy to Clipboard"); action: "win.copy"; } + section { + label: _("Style"); + + item { + label: _("Follow System"); + action: "app.color-scheme"; + target: "default"; + } + + item { + label: _("Light"); + action: "app.color-scheme"; + target: "force-light"; + } + + item { + label: _("Dark"); + action: "app.color-scheme"; + target: "force-dark"; + } + } + section { item { label: _("Keyboard Shortcuts"); diff --git a/src/window.js b/src/window.js index 7d75f8b..a4f7b5b 100644 --- a/src/window.js +++ b/src/window.js @@ -11,12 +11,43 @@ import AppButton, { ShowInFolderButton } from "./AppButton.js"; import { settings } from "./common.js"; import Interface from "./window.blp" with { type: "uri" }; +const WINDOW_WIDTH = 772; +const WINDOW_HEIGHT = 218; +const WINDOW_WIDTH_COMPACT = 560; +// tile 80 + .list padding 2x6 + the 48px bottom bar, the way 218 is +// tile 134 + .list padding 2x12 + the bottom bar +const WINDOW_HEIGHT_COMPACT = 140; +const WINDOW_WIDTH_MIN = 360; + export default function Window({ application, file }) { const { window, list, entry } = build(Interface); if (__DEV__) window.add_css_class("devel"); window.set_application(application); + // The tiles shrink in compact mode, but the window keeps whatever size it + // already has, so it is resized explicitly on top of the .compact styles. + function applyCompact() { + const compact = settings.get_boolean("compact"); + + if (compact) window.add_css_class("compact"); + else window.remove_css_class("compact"); + + window.set_size_request( + WINDOW_WIDTH_MIN, + compact ? WINDOW_HEIGHT_COMPACT : WINDOW_HEIGHT, + ); + window.set_default_size( + compact ? WINDOW_WIDTH_COMPACT : WINDOW_WIDTH, + compact ? WINDOW_HEIGHT_COMPACT : WINDOW_HEIGHT, + ); + } + const compact_handler = settings.connect("changed::compact", applyCompact); + window.connect("destroy", () => { + settings.disconnect(compact_handler); + }); + applyCompact(); + const { content_type, resource, scheme } = readResource(file); Entry({ @@ -29,12 +60,13 @@ export default function Window({ application, file }) { const options = []; - applications.forEach((appInfo) => { + applications.forEach((appInfo, index) => { const button = AppButton({ appInfo, content_type, entry, window, + position: index + 1, }); appInfo.button = button; options.push(button); @@ -54,6 +86,7 @@ export default function Window({ application, file }) { file, entry, window, + position: options.length + 1, }); options.push(button); list.append( @@ -66,9 +99,10 @@ export default function Window({ application, file }) { function getButtonForKeyval(keyval) { const keyname = Gdk.keyval_name(keyval); - // Is not 0...9 - if (!/^\d$/.test(keyname)) return null; - const n = +keyname; + // Is not 0...9, on the number row or on the numeric keypad + const digit = /^(?:KP_)?(\d)$/.exec(keyname); + if (!digit) return null; + const n = +digit[1]; return options[n - 1]; } @@ -100,6 +134,9 @@ export default function Window({ application, file }) { const toggleAppNames = settings.create_action("show-app-names"); window.add_action(toggleAppNames); + const toggleCompact = settings.create_action("compact"); + window.add_action(toggleCompact); + const run_action = new Gio.SimpleAction({ name: "run_action", parameter_type: new GLib.VariantType("a{ss}"),