Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 223 additions & 25 deletions Bar.qml

Large diffs are not rendered by default.

91 changes: 70 additions & 21 deletions core/LayoutController.qml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ Item {
property var stateService: null
readonly property var config: stateService && stateService.config
? stateService.config : ({})
readonly property bool v2Mode: config && config.presentation
? String(config.presentation.shellStyle || "shibumi") !== "shibumi"
: false
readonly property bool v2Mode: {
const source = stateService && stateService.config
? stateService.config : config
return source && source.presentation
? String(source.presentation.shellStyle || "shibumi") !== "shibumi"
: false
}
readonly property var layoutProtection: config && config.layoutProtection
? config.layoutProtection : ({ v1: false, v2: false })
readonly property bool v1LayoutProtected: layoutProtection.v1 === true
Expand Down Expand Up @@ -93,13 +97,25 @@ Item {
LayoutModel.copySplits(nextSplits, nextOrder))
}

function persistV2Layout(nextSlots) {
if (!stateService || typeof stateService.setV2Layout !== "function")
return false
const previousSlots = V2LayoutModel.copy(v2Slots)
if (!previousSlots || !stateService.setV2Layout(nextSlots)) return false
if (bar && typeof bar.syncV2DynamicLayout === "function"
&& !bar.syncV2DynamicLayout(nextSlots)) {
if (!stateService.setV2Layout(previousSlots))
console.warn("V2 layout rollback failed after host sync rejection")
return false
}
return true
}

function swapGroups(sourceGroupId, targetGroupId) {
if (v2Mode) {
const nextSlots = V2LayoutModel.swapGroups(
v2Slots, sourceGroupId, targetGroupId)
return nextSlots && stateService
&& typeof stateService.setV2Layout === "function"
? stateService.setV2Layout(nextSlots) : false
return nextSlots ? persistV2Layout(nextSlots) : false
}
const currentOrder = currentV1Order()
const nextOrder = LayoutModel.swapGroups(
Expand All @@ -109,11 +125,9 @@ Item {

function moveGroupToSlot(sourceGroupId, targetRegion, targetIndex) {
if (v2Mode) {
if (!stateService || typeof stateService.setV2Layout !== "function")
return false
const nextSlots = V2LayoutModel.moveGroupToSlot(
v2Slots, sourceGroupId, targetRegion, targetIndex)
return nextSlots ? stateService.setV2Layout(nextSlots) : false
return nextSlots ? persistV2Layout(nextSlots) : false
}
const currentOrder = currentV1Order()
const nextOrder = LayoutModel.moveGroupToSlot(
Expand Down Expand Up @@ -158,6 +172,29 @@ Item {
return persist(next.order, next.splits)
}

function reconcileV2PluginGroups(specs, syncValue, followRegionsValue) {
if (!v2Mode || !stateService
|| typeof stateService.setV2Layout !== "function") return false
const current = V2LayoutModel.copy(v2Slots)
const next = V2LayoutModel.reconcilePluginGroups(
current, specs, followRegionsValue === true)
if (!next || next.unplaced.length > 0) return false
if (V2LayoutModel.same(current, next.layout)) {
return syncValue === true && bar
&& typeof bar.syncV2DynamicLayout === "function"
? bar.syncV2DynamicLayout(next.layout) : true
}
if (!stateService.setV2Layout(next.layout)) return false
if (syncValue === true && bar
&& typeof bar.syncV2DynamicLayout === "function"
&& !bar.syncV2DynamicLayout(next.layout)) {
if (!stateService.setV2Layout(current))
console.warn("V2 reconciliation rollback failed after host sync rejection")
return false
}
return true
}

function baseV1SlotCount(region) {
return LayoutModel.baseCount(region)
}
Expand All @@ -171,17 +208,15 @@ Item {
}

function addV2Slot(region) {
if (!v2Mode || !stateService
|| typeof stateService.setV2Layout !== "function") return false
if (!v2Mode) return false
const next = V2LayoutModel.addSlot(v2Slots, region)
return next ? stateService.setV2Layout(next) : false
return next ? persistV2Layout(next) : false
}

function removeV2Slot(region) {
if (!v2Mode || !stateService
|| typeof stateService.setV2Layout !== "function") return false
if (!v2Mode) return false
const next = V2LayoutModel.removeSlot(v2Slots, region)
return next ? stateService.setV2Layout(next) : false
return next ? persistV2Layout(next) : false
}

function baseV2SlotCount(region) {
Expand All @@ -195,10 +230,9 @@ Item {
}

function removeV2SlotAt(region, index) {
if (!v2Mode || !stateService
|| typeof stateService.setV2Layout !== "function") return false
if (!v2Mode) return false
const next = V2LayoutModel.removeSlotAt(v2Slots, region, index)
return next ? stateService.setV2Layout(next) : false
return next ? persistV2Layout(next) : false
}

function toggleSplit(region, index, editingValue) {
Expand All @@ -223,10 +257,25 @@ Item {
return nextSplits ? persist(currentOrder, nextSplits) : false
}

function resetV2Layout() {
if (!v2Mode || !stateService
|| typeof stateService.resetV2Layout !== "function") return false
if (!stateService.resetV2Layout()) return false
return !bar || typeof bar.syncV2DynamicLayout !== "function"
? true : bar.syncV2DynamicLayout(V2LayoutModel.defaultLayout())
}

function restoreV2Layout(value) {
if (!v2Mode || !stateService
|| typeof stateService.setV2Layout !== "function") return false
const target = V2LayoutModel.copy(value)
if (!target) return false
return V2LayoutModel.same(v2Slots, target)
|| stateService.setV2Layout(target)
}

function resetLayout() {
if (v2Mode && stateService
&& typeof stateService.resetV2Layout === "function")
return stateService.resetV2Layout()
if (v2Mode) return resetV2Layout()
return stateService && typeof stateService.resetLayout === "function"
? stateService.resetLayout() : false
}
Expand Down
15 changes: 12 additions & 3 deletions core/ShibumiConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,13 @@ function isV1DynamicGroupId(value) {
&& /^[a-z0-9][a-z0-9._-]*$/.test(pluginId)
}

function isDynamicGroupId(value) {
return isV1DynamicGroupId(value)
}

function isGroupId(value) {
var groupId = String(value || "")
return GroupIds.indexOf(groupId) >= 0 || isV1DynamicGroupId(groupId)
return GroupIds.indexOf(groupId) >= 0 || isDynamicGroupId(groupId)
}

function boolArray(value, length) {
Expand Down Expand Up @@ -241,12 +245,17 @@ function normalizedV2Layout(value) {
return null
for (var i = 0; i < entries.length; i++) {
var id = String(entries[i] || "")
if (id !== "" && (GroupIds.indexOf(id) < 0 || seen[id])) return null
if (id !== "" && ((GroupIds.indexOf(id) < 0
&& !isDynamicGroupId(id)) || seen[id])) return null
if (id !== "") seen[id] = true
result[region].push(id)
}
}
return Object.keys(seen).length === GroupIds.length ? result : null
var fixedCount = 0
for (var groupId in seen) {
if (GroupIds.indexOf(groupId) >= 0) fixedCount++
}
return fixedCount === GroupIds.length ? result : null
}

function normalizedV2Boundaries(value) {
Expand Down
167 changes: 163 additions & 4 deletions core/V2LayoutModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var GroupIds = [
"G16", "G17", "G18"
]
var Regions = ["left", "center", "right"]
var DynamicGroupPrefix = "G:"
var Limits = {
left: { min: 10, max: 13 },
center: { min: 1, max: 4 },
Expand All @@ -16,6 +17,28 @@ function isObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value)
}

function validPluginId(value) {
var id = String(value || "")
return id.length > 0 && id.length <= 160
&& /^[a-z0-9][a-z0-9._-]*$/.test(id)
}

function dynamicGroupId(pluginValue) {
var pluginId = String(pluginValue || "")
return validPluginId(pluginId) ? DynamicGroupPrefix + pluginId : ""
}

function dynamicPluginId(groupValue) {
var groupId = String(groupValue || "")
if (groupId.indexOf(DynamicGroupPrefix) !== 0) return ""
var pluginId = groupId.slice(DynamicGroupPrefix.length)
return validPluginId(pluginId) ? pluginId : ""
}

function isDynamicGroupId(value) {
return dynamicPluginId(value) !== ""
}

function defaultLayout() {
return {
left: ["G1", "G2", "G3", "", "G5", "G6", "G4", "G7", "", ""],
Expand All @@ -29,7 +52,7 @@ function defaultLayout() {

function valid(value) {
if (!isObject(value)) return false
var seen = {}
var seen = Object.create(null)
for (var r = 0; r < Regions.length; r++) {
var region = Regions[r]
var entries = value[region]
Expand All @@ -39,11 +62,16 @@ function valid(value) {
return false
for (var i = 0; i < entries.length; i++) {
var id = String(entries[i] || "")
if (id !== "" && (GroupIds.indexOf(id) < 0 || seen[id])) return false
if (id !== "" && ((GroupIds.indexOf(id) < 0
&& !isDynamicGroupId(id)) || seen[id])) return false
if (id !== "") seen[id] = true
}
}
return Object.keys(seen).length === GroupIds.length
var fixedCount = 0
for (var groupId in seen) {
if (GroupIds.indexOf(groupId) >= 0) fixedCount++
}
return fixedCount === GroupIds.length
}

function copy(value) {
Expand All @@ -67,7 +95,7 @@ function visibleOrder(value) {
function locationFor(value, groupValue) {
var source = valid(value) ? value : defaultLayout()
var groupId = String(groupValue || "")
if (GroupIds.indexOf(groupId) < 0) return null
if (GroupIds.indexOf(groupId) < 0 && !isDynamicGroupId(groupId)) return null
for (var r = 0; r < Regions.length; r++) {
var region = Regions[r]
var index = source[region].indexOf(groupId)
Expand Down Expand Up @@ -138,6 +166,137 @@ function removeSlotAt(value, regionValue, indexValue) {
return valid(result) ? result : null
}

function normalizedPluginSpecs(value) {
if (!Array.isArray(value)) return null
var result = []
var seen = Object.create(null)
for (var i = 0; i < value.length; i++) {
var spec = value[i]
if (!isObject(spec) || !validPluginId(spec.pluginId)) return null
var pluginId = String(spec.pluginId)
if (seen[pluginId]) continue
seen[pluginId] = true
var region = String(spec.region || "")
result.push({
pluginId: pluginId,
region: Regions.indexOf(region) >= 0 ? region : "right"
})
}
result.sort(function(left, right) {
return left.pluginId.localeCompare(right.pluginId)
})
return result
}

function compactEmptyTail(value, regionValue) {
var region = String(regionValue || "")
var limit = Limits[region]
if (!limit || !Array.isArray(value[region])) return false
var changed = false
while (value[region].length > limit.min
&& String(value[region][value[region].length - 1] || "") === "") {
value[region].pop()
changed = true
}
return changed
}

function addDynamicGroup(value, pluginValue, regionValue) {
var result = copy(valid(value) ? value : defaultLayout())
var groupId = dynamicGroupId(pluginValue)
var region = String(regionValue || "")
if (!result || groupId === "" || Regions.indexOf(region) < 0) return null
if (locationFor(result, groupId)) return result
for (var index = 0; index < result[region].length; index++) {
if (String(result[region][index] || "") !== "") continue
result[region][index] = groupId
return valid(result) ? result : null
}
if (result[region].length >= Limits[region].max) return null
result[region].push(groupId)
return valid(result) ? result : null
}

function moveDynamicGroupToRegion(value, groupValue, regionValue) {
var result = copy(valid(value) ? value : defaultLayout())
var groupId = String(groupValue || "")
var region = String(regionValue || "")
var source = locationFor(result, groupId)
if (!result || !source || !isDynamicGroupId(groupId)
|| Regions.indexOf(region) < 0) return null
if (source.region === region) return result
var targetIndex = -1
for (var index = 0; index < result[region].length; index++) {
if (String(result[region][index] || "") === "") {
targetIndex = index
break
}
}
if (targetIndex < 0 && result[region].length < Limits[region].max) {
result[region].push("")
targetIndex = result[region].length - 1
}
if (targetIndex < 0) targetIndex = 0
return moveGroupToSlot(result, groupId, region, targetIndex)
}

function removeDynamicGroup(value, groupValue) {
var result = copy(valid(value) ? value : defaultLayout())
var groupId = String(groupValue || "")
var location = locationFor(result, groupId)
if (!result || !isDynamicGroupId(groupId) || !location) return null
result[location.region][location.index] = ""
compactEmptyTail(result, location.region)
return valid(result) ? result : null
}

function reconcilePluginGroups(value, specsValue, followRegionsValue) {
var result = copy(valid(value) ? value : defaultLayout())
var specs = normalizedPluginSpecs(specsValue)
var followRegions = followRegionsValue === true
if (!result || !specs) return null
var desired = Object.create(null)
for (var i = 0; i < specs.length; i++)
desired[dynamicGroupId(specs[i].pluginId)] = specs[i]

var stale = []
for (var r = 0; r < Regions.length; r++) {
var region = Regions[r]
for (var index = 0; index < result[region].length; index++) {
var groupId = String(result[region][index] || "")
if (isDynamicGroupId(groupId) && !desired[groupId]
&& stale.indexOf(groupId) < 0)
stale.push(groupId)
}
}
for (var staleIndex = 0; staleIndex < stale.length; staleIndex++) {
result = removeDynamicGroup(result, stale[staleIndex])
if (!result) return null
}

var unplaced = []
for (var specIndex = 0; specIndex < specs.length; specIndex++) {
var spec = specs[specIndex]
var requestedGroupId = dynamicGroupId(spec.pluginId)
var currentLocation = locationFor(result, requestedGroupId)
if (currentLocation && followRegions
&& currentLocation.region !== spec.region) {
var moved = moveDynamicGroupToRegion(
result, requestedGroupId, spec.region)
if (!moved) unplaced.push(spec.pluginId)
else result = moved
continue
}
if (currentLocation) continue
var placed = addDynamicGroup(result, spec.pluginId, spec.region)
if (!placed) unplaced.push(spec.pluginId)
else result = placed
}
// A full region must not silently publish a partially placed V2 layout.
if (unplaced.length > 0) return { layout: result, unplaced: unplaced }
return { layout: result, unplaced: [] }
}

function same(left, right) {
if (!valid(left) || !valid(right)) return false
return JSON.stringify(left) === JSON.stringify(right)
Expand Down
Loading