2023-01-06 14:07:33 -05:00
|
|
|
/*
|
|
|
|
* Copyright (C) 2023 Savoir-faire Linux Inc.
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import QtQuick
|
|
|
|
import QtQuick.Controls
|
|
|
|
import QtQuick.Layouts
|
|
|
|
|
|
|
|
import net.jami.Constants 1.1
|
|
|
|
import net.jami.Models 1.1
|
|
|
|
|
|
|
|
// This object should be implemented as a QML singleton, or be instantiated
|
|
|
|
// once in the main application window component. The top-level application window
|
|
|
|
// contains a loader[mainview, wizardview] and "rootView" MUST parent a horizontal
|
|
|
|
// SplitView with a StackView in each pane.
|
|
|
|
QtObject {
|
|
|
|
id: root
|
|
|
|
|
2023-02-24 17:20:37 -05:00
|
|
|
required property QtObject viewManager
|
|
|
|
|
2023-01-06 14:07:33 -05:00
|
|
|
signal requestAppWindowWizardView
|
|
|
|
|
|
|
|
// A map of view names to file paths for QML files that define each view.
|
2023-02-24 19:05:26 -05:00
|
|
|
property variant resources: {
|
|
|
|
"WelcomePage": "mainview/components/WelcomePage.qml",
|
|
|
|
"SidePanel": "mainview/components/SidePanel.qml",
|
|
|
|
"ConversationView": "mainview/ConversationView.qml",
|
|
|
|
"NewSwarmPage": "mainview/components/NewSwarmPage.qml",
|
|
|
|
"WizardView": "wizardview/WizardView.qml",
|
|
|
|
"SettingsView": "settingsview/SettingsView.qml",
|
|
|
|
}
|
2023-01-06 14:07:33 -05:00
|
|
|
|
|
|
|
// Maybe this state needs to be toggled because the SidePanel content is replaced.
|
|
|
|
// This makes it so the state can't be inferred from loaded views in single pane mode.
|
|
|
|
property bool inSettings: viewManager.hasView("SettingsView")
|
2023-02-24 19:05:26 -05:00
|
|
|
property bool inWizard: viewManager.hasView("WizardView")
|
2023-01-06 14:07:33 -05:00
|
|
|
property bool inNewSwarm: viewManager.hasView("NewSwarmPage")
|
2023-02-24 19:05:26 -05:00
|
|
|
property bool inhibitConversationView: inSettings || inWizard || inNewSwarm
|
2023-01-06 14:07:33 -05:00
|
|
|
|
|
|
|
property bool busy: false
|
|
|
|
|
|
|
|
// The `main` view of the application window.
|
|
|
|
property Item rootView: null
|
|
|
|
|
|
|
|
// HACKS.
|
|
|
|
property real mainViewWidth: rootView ? rootView.width : 0
|
|
|
|
property real previousWidth: mainViewWidth
|
|
|
|
property real mainViewSidePanelRectWidth: sv1 ? sv1.width : 0
|
|
|
|
property real lastSideBarSplitSize: mainViewSidePanelRectWidth
|
|
|
|
onMainViewWidthChanged: resolvePanes()
|
|
|
|
|
|
|
|
function resolvePanes(force=false) {
|
|
|
|
if (forceSinglePane) return
|
|
|
|
const isExpanding = previousWidth < mainViewWidth
|
|
|
|
if (mainViewWidth < JamiTheme.chatViewHeaderMinimumWidth + mainViewSidePanelRectWidth
|
|
|
|
&& sv2.visible && (!isExpanding || force)) {
|
|
|
|
lastSideBarSplitSize = mainViewSidePanelRectWidth
|
|
|
|
singlePane = true
|
|
|
|
} else if (mainViewWidth >= lastSideBarSplitSize + JamiTheme.chatViewHeaderMinimumWidth
|
|
|
|
&& !sv2.visible && (isExpanding || force) && !layoutManager.isFullScreen) {
|
|
|
|
singlePane = false
|
|
|
|
}
|
|
|
|
previousWidth = mainViewWidth
|
|
|
|
}
|
|
|
|
|
|
|
|
// Must be the child of `rootView`.
|
|
|
|
property Item splitView: null
|
|
|
|
|
|
|
|
// StackView objects, which are children of `splitView`.
|
|
|
|
property StackView sv1: null
|
|
|
|
property StackView sv2: null
|
|
|
|
|
|
|
|
// The StackView object that is currently active, determined by the value
|
|
|
|
// of singlePane.
|
|
|
|
readonly property StackView activeStackView: singlePane ? sv1 : sv2
|
|
|
|
|
|
|
|
readonly property string currentViewName: {
|
|
|
|
if (activeStackView == null || activeStackView.depth === 0) return ''
|
|
|
|
return activeStackView.currentItem.objectName
|
|
|
|
}
|
|
|
|
|
|
|
|
readonly property var currentView: {
|
|
|
|
return activeStackView ? activeStackView.currentItem : null
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle single/dual pane mode.
|
|
|
|
property bool forceSinglePane: false
|
|
|
|
property bool singlePane
|
|
|
|
onForceSinglePaneChanged: {
|
|
|
|
if (forceSinglePane) singlePane = true
|
|
|
|
else resolvePanes(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
onSinglePaneChanged: {
|
|
|
|
// Hiding sv2 before moving items from, and after moving
|
|
|
|
// items to, reduces stack item visibility change events.
|
|
|
|
if (singlePane) {
|
|
|
|
sv2.visible = false
|
|
|
|
if (forceSinglePane) Qt.callLater(move, sv2, sv1)
|
|
|
|
else move(sv2, sv1)
|
|
|
|
} else {
|
|
|
|
move(sv1, sv2)
|
|
|
|
sv2.visible = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Emitted once at the end of setRootView.
|
|
|
|
signal initialized()
|
|
|
|
|
|
|
|
// Create, present, and return a dialog object.
|
|
|
|
function presentDialog(parent, path, props={}) {
|
|
|
|
// Open the dialog once the object is created
|
|
|
|
return viewManager.createView(path, parent, function(obj) {
|
|
|
|
const doneCb = function() { viewManager.destroyView(path) }
|
|
|
|
if (obj.closed !== undefined) {
|
|
|
|
obj.closed.connect(doneCb)
|
|
|
|
} else {
|
|
|
|
if (obj.accepted !== undefined) { obj.accepted.connect(doneCb) }
|
|
|
|
if (obj.rejected !== undefined) { obj.rejected.connect(doneCb) }
|
|
|
|
}
|
|
|
|
obj.open()
|
|
|
|
}, props)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Dismiss all views.
|
|
|
|
function dismissAll() {
|
|
|
|
for (var path in viewManager.views) {
|
|
|
|
viewManager.destroyView(path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get a view regardless of whether it is currently active.
|
|
|
|
function getView(viewName) {
|
|
|
|
if (!viewManager.hasView(viewName)) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
return viewManager.views[viewManager.viewPaths[viewName]]
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sets object references, onInitialized is a good time to present views.
|
|
|
|
function setRootView(obj) {
|
|
|
|
rootView = obj
|
|
|
|
splitView = rootView.splitView
|
|
|
|
sv1 = rootView.sv1
|
|
|
|
sv1.parent = Qt.binding(() => singlePane ? rootView : splitView)
|
|
|
|
sv1.anchors.fill = Qt.binding(() => singlePane ? rootView : undefined)
|
|
|
|
sv2 = rootView.sv2
|
|
|
|
|
|
|
|
initialized()
|
|
|
|
resolvePanes()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Finds a view and gets its index within the StackView it's in.
|
|
|
|
function getStackIndex(viewName) {
|
|
|
|
for (const [key, value] of Object.entries(viewManager.views)) {
|
|
|
|
if (value.objectName === viewName) {
|
|
|
|
return value.StackView.index
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
2023-02-24 19:38:00 -05:00
|
|
|
// Load a view without presenting it.
|
|
|
|
function preload(viewName) {
|
|
|
|
if (!viewManager.createView(resources[viewName], null)) {
|
|
|
|
console.log("Failed to load view: " + viewName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 14:07:33 -05:00
|
|
|
// This function presents the view with the given viewName in the
|
|
|
|
// specified StackView. Return the view if successful.
|
|
|
|
function present(viewName, sv=activeStackView) {
|
|
|
|
if (!rootView) return
|
|
|
|
|
2023-02-24 19:05:26 -05:00
|
|
|
if (viewName === "ConversationView" && inhibitConversationView) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-01-06 14:07:33 -05:00
|
|
|
// If the view already exists in the StackView, the function will attempt
|
|
|
|
// to navigate to its StackView position by dismissing elevated views.
|
|
|
|
if (sv.find(function(item) {
|
|
|
|
return item.objectName === viewName;
|
|
|
|
})) {
|
|
|
|
const viewIndex = getStackIndex(viewName)
|
|
|
|
if (viewIndex >= 0) {
|
|
|
|
for (var i = (sv.depth - 1); i > viewIndex; i--) {
|
|
|
|
dismissObj(sv.get(i, StackView.DontLoad))
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we are in single-pane mode and the view was previously forced into
|
|
|
|
// sv2, we can move it back to the top of sv1.
|
|
|
|
if (singlePane && sv === sv1) {
|
|
|
|
// See if the item is at the top of sv2
|
|
|
|
if (sv2.currentItem && sv2.currentItem.objectName === viewName) {
|
|
|
|
// Move it to the top of sv1
|
|
|
|
const view = sv2.pop(StackView.Immediate)
|
|
|
|
sv1.push(view, StackView.Immediate)
|
|
|
|
view.presented()
|
|
|
|
return view
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const obj = viewManager.createView(resources[viewName], appWindow)
|
|
|
|
if (!obj) {
|
|
|
|
print("could not create view:", viewName)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
if (obj === currentView) {
|
|
|
|
print("view is current:", viewName)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we are in single-pane mode and the view should start hidden
|
|
|
|
// (requiresIndex), we can push it into sv2.
|
|
|
|
if (singlePane && sv === sv1 && obj.requiresIndex) {
|
|
|
|
sv = sv2
|
|
|
|
} else {
|
|
|
|
forceSinglePane = obj.singlePaneOnly
|
|
|
|
}
|
|
|
|
|
|
|
|
const view = sv.push(obj, StackView.Immediate)
|
|
|
|
if (!view) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
if (view.objectName === '') {
|
|
|
|
view.objectName = viewName
|
|
|
|
}
|
|
|
|
view.presented()
|
|
|
|
return view
|
|
|
|
}
|
|
|
|
|
|
|
|
// Dismiss by object.
|
|
|
|
function dismissObj(obj, sv=activeStackView) {
|
|
|
|
if (obj.StackView.view !== sv) {
|
|
|
|
print("view not in the stack:", obj)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we are dismissing a view that is not at the top of the stack,
|
|
|
|
// we need to store each of the views on top into a temporary stack
|
|
|
|
// and then restore them after the view is dismissed.
|
|
|
|
// So we get the index of the view we are dismissing.
|
|
|
|
const viewIndex = obj.StackView.index
|
|
|
|
var tempStack = []
|
|
|
|
for (var i = (sv.depth - 1); i > viewIndex; i--) {
|
|
|
|
var item = sv.pop(StackView.Immediate)
|
|
|
|
tempStack.push(item)
|
|
|
|
}
|
|
|
|
// And we define a function to restore and resolve the views.
|
|
|
|
var resolveStack = () => {
|
|
|
|
for (var i = 0; i < tempStack.length; i++) {
|
|
|
|
sv.push(tempStack[i], StackView.Immediate)
|
|
|
|
}
|
|
|
|
|
|
|
|
forceSinglePane = sv.currentItem.singlePaneOnly
|
|
|
|
sv.currentItem.presented()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now we can dismiss the view at the top of the stack.
|
|
|
|
const depth = sv.depth
|
|
|
|
if (obj === sv.get(depth - 1, StackView.DontLoad)) {
|
|
|
|
var view = sv.pop(StackView.Immediate)
|
|
|
|
if (!view) {
|
|
|
|
print("could not pop view:", obj.objectName)
|
|
|
|
resolveStack()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the view is managed, we can destroy it, otherwise, it can
|
|
|
|
// be reused and destroyed by it's parent.
|
|
|
|
if (view.managed) {
|
|
|
|
var objectName = view ? view.objectName : obj.objectName
|
|
|
|
if (!viewManager.destroyView(resources[objectName])) {
|
|
|
|
print("could not destroy view:", objectName)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
view.dismissed()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
resolveStack()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Dismiss by view name.
|
|
|
|
function dismiss(viewName) {
|
2023-02-24 19:38:00 -05:00
|
|
|
if (!rootView) return
|
|
|
|
|
2023-01-06 14:07:33 -05:00
|
|
|
const depth = activeStackView.depth
|
|
|
|
for (var i = (depth - 1); i >= 0; i--) {
|
|
|
|
const view = activeStackView.get(i, StackView.DontLoad)
|
|
|
|
if (view.objectName === viewName) {
|
|
|
|
dismissObj(view)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if the view is hidden on the top of sv2 (if in single-pane mode),
|
|
|
|
// and dismiss it in that case.
|
|
|
|
if (singlePane && sv2.currentItem && sv2.currentItem.objectName === viewName) {
|
|
|
|
dismissObj(sv2.currentItem, sv2)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Move items from one stack to another. We avoid the recursive technique to
|
|
|
|
// avoid visibility change events.
|
|
|
|
function move(from, to, depth=1) {
|
|
|
|
busy = true
|
|
|
|
var tempStack = []
|
|
|
|
while (from.depth > depth) {
|
|
|
|
var item = from.pop(StackView.Immediate)
|
|
|
|
tempStack.push(item)
|
|
|
|
}
|
|
|
|
while (tempStack.length) {
|
|
|
|
to.push(tempStack.pop(), StackView.Immediate)
|
|
|
|
}
|
|
|
|
busy = false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Effectively hide the current view by moving it to the other StackView.
|
|
|
|
// This function only works when in single-pane mode.
|
|
|
|
function hideCurrentView() {
|
|
|
|
if (singlePane) move(sv1, sv2)
|
|
|
|
}
|
|
|
|
}
|