From 5929a9b80cb4949591fb497e882f3f7ac157d240 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 27 Sep 2026 21:26:00 +0200 Subject: [PATCH] Set Elsewhen's here by tapping a city on the globe (#13438) A city on this machine's zone becomes homeCity, the zone's own city clears it, and a city on another zone leaves it alone. Ported from omacom/elsewhen#5. Co-authored-by: Shawn Yeager Co-authored-by: Claude Opus 5.5 --- docs/elsewhen.md | 4 ++-- shell/plugins/panels/elsewhen/Globe.qml | 6 +++++- shell/plugins/panels/elsewhen/Model.js | 13 +++++++++++++ shell/plugins/panels/elsewhen/Panel.qml | 8 ++++++++ test/shell.d/elsewhen-model-test.sh | 10 ++++++++++ 5 files changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/elsewhen.md b/docs/elsewhen.md index af703696..db6e2cc1 100644 --- a/docs/elsewhen.md +++ b/docs/elsewhen.md @@ -302,7 +302,7 @@ The opening spin **lands on home** - the animation runs from `homeLon - 1080` to The header reads "It's 10:28 AM here in Los Angeles." rather than a bare "here", which names your own city without spending a row on it. The zone comes from the same `date` probe the rows use - one extra `LOCAL|` line, from `timedatectl` - so it costs no additional process and follows a time-zone change on the next refresh. -The city name is the zone's last segment, and the tz database names zones after a *representative* city: someone in Boca Raton would read "here in New York". Set `homeCity` to override it. +The city name is the zone's last segment, and the tz database names zones after a *representative* city: someone in Boca Raton would read "here in New York". Tap your city on the globe to fix that: a city on this machine's zone becomes `homeCity`, and the header and home pin follow it. Tapping the zone's own city clears the choice, and a city on another zone leaves it alone, since the time beside the name is still this machine's. ## Two offsets, one line @@ -369,7 +369,7 @@ Inline on the widget's `shell.json` entry: | `offsetMode` | `home` for the offset from you (default), `utc` for the absolute one | | `units` | `F` or `C`; blank (the default) follows the system's measurement units. Click any temperature to flip it | | `globeEnabled` | `false` to remove the globe entry point (default on) | -| `homeCity` | your city for the header (blank = from the system zone) | +| `homeCity` | your city for the header (blank = from the system zone); set by tapping a city on this zone on the globe | | `smoothMotion` | drop labels and detail while the globe moves (default true) | ## IPC diff --git a/shell/plugins/panels/elsewhen/Globe.qml b/shell/plugins/panels/elsewhen/Globe.qml index 4ca3d65c..f14c7f23 100644 --- a/shell/plugins/panels/elsewhen/Globe.qml +++ b/shell/plugins/panels/elsewhen/Globe.qml @@ -56,6 +56,7 @@ Item { signal offsetModeToggleRequested() // Only real selections: -1 means "no city" here but "home" in the list. signal citySelected(string label, string zone) + signal cityTapped(string label, string zone) // ---- data ------------------------------------------------------------- property var land: [] // coastline rings, flat [lon,lat,...] @@ -96,7 +97,10 @@ Item { function pickAt(cx, cy) { var hit = hitAt(cx, cy) selectAt(hit) - if (hit >= 0) flyTo(allCities[hit][2], allCities[hit][3]) + if (hit >= 0) { + flyTo(allCities[hit][2], allCities[hit][3]) + cityTapped(allCities[hit][0], allCities[hit][1]) + } return hit } diff --git a/shell/plugins/panels/elsewhen/Model.js b/shell/plugins/panels/elsewhen/Model.js index 4f3d041e..16ed9a85 100644 --- a/shell/plugins/panels/elsewhen/Model.js +++ b/shell/plugins/panels/elsewhen/Model.js @@ -272,6 +272,18 @@ function labelForZoneId(id) { return String(id || "").split("/").pop().replace(/_/g, " ") } +// Tapping a city on this machine's clock makes it "here", and the zone's own +// city clears the choice. The new homeCity setting, or null to leave it. +function homeCityAfterTap(label, zoneId, homeOverride, localZone) { + var name = String(label || "").trim() + var local = String(localZone || "") + if (name === "" || local === "" || String(zoneId || "") !== local) return null + var override = String(homeOverride || "").trim() + var zoneCity = labelForZoneId(local) + if (name === (override === "" ? zoneCity : override)) return null + return name === zoneCity ? "" : name +} + function serializeZones(zones) { var parts = [] for (var i = 0; i < zones.length; i++) { @@ -880,6 +892,7 @@ if (typeof module !== "undefined") { indexOfZone: indexOfZone, indexOfZoneKey: indexOfZoneKey, labelForZoneId: labelForZoneId, + homeCityAfterTap: homeCityAfterTap, serializeZones: serializeZones, addZone: addZone, removeZoneAt: removeZoneAt, diff --git a/shell/plugins/panels/elsewhen/Panel.qml b/shell/plugins/panels/elsewhen/Panel.qml index ff00c76c..74ebbf0a 100644 --- a/shell/plugins/panels/elsewhen/Panel.qml +++ b/shell/plugins/panels/elsewhen/Panel.qml @@ -118,6 +118,13 @@ Panel { else g.showHome() } + function claimHome(label, zone) { + var stored = Model.homeCityAfterTap(label, zone, setting("homeCity", ""), localZone) + if (stored === null) return + persistSettings({ homeCity: stored }) + refreshFacts() + } + function focusFromGlobe(label, zone) { var i = Model.indexOfZone(zones, label, zone) if (i >= 0) focusOn(i) @@ -876,6 +883,7 @@ Panel { item.exitRequested.connect(function() { root.setGlobeMode(false, false) }) item.jumpDismissed.connect(function() { Qt.callLater(function() { keyCatcher.forceActiveFocus() }) }) item.citySelected.connect(function(label, zone) { root.focusFromGlobe(label, zone) }) + item.cityTapped.connect(function(label, zone) { root.claimHome(label, zone) }) // A cold start can finish loading after the mode switched on. if (root.globeMode) root.showFocusOnGlobe() } diff --git a/test/shell.d/elsewhen-model-test.sh b/test/shell.d/elsewhen-model-test.sh index 05234e87..00dc2955 100644 --- a/test/shell.d/elsewhen-model-test.sh +++ b/test/shell.d/elsewhen-model-test.sh @@ -306,6 +306,16 @@ assertDeepEqual(seedProblems, [], 'elsewhen seeds four well-spread cities around assertEqual(M.seedZones({ label: 'Nowhere', id: 'Not/AZone' }, offsets, 4).length, 1, 'elsewhen seeds only home when its offset is unknown') assertEqual(M.pickSeedZones({ label: 'X', id: 'UTC' }, {}, 4).length, 0, 'elsewhen seeds nothing without offsets') +// ---- claiming home from the globe +const CHI = 'America/Chicago' +assertEqual(M.homeCityAfterTap('Nashville', CHI, '', CHI), 'Nashville', 'elsewhen makes a tapped city on this zone home') +assertEqual(M.homeCityAfterTap('Nashville', CHI, 'Nashville', CHI), null, 'elsewhen leaves home alone on a second tap') +assertEqual(M.homeCityAfterTap('Chicago', CHI, 'Nashville', CHI), '', 'elsewhen clears home when the zone\'s own city is tapped') +assertEqual(M.homeCityAfterTap('Chicago', CHI, '', CHI), null, 'elsewhen stores nothing for the zone\'s own city when none is set') +assertEqual(M.homeCityAfterTap('London', 'Europe/London', '', CHI), null, 'elsewhen never makes a city on another zone home') +assertEqual(M.homeCityAfterTap('Nashville', CHI, '', ''), null, 'elsewhen stores nothing before the machine zone is known') +assertEqual(M.homeCityAfterTap(' Nashville ', CHI, '', CHI), 'Nashville', 'elsewhen trims a tapped name') + // ---- coordinates and weather const tab = '#comment\nUS\t+404251-0740023\tAmerica/New_York\nIN\t+2232+08822\tAsia/Kolkata\n' assertDeepEqual(M.zoneTabCoords(tab, 'America/New_York'), { lat: 40.7142, lon: -74.0064 }, 'elsewhen reads seconds-precision zone1970 coordinates')