1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-08-03 22:35:45 +02:00

misc: implement frameless window

Several major changes to the layout have been made.
- The chat search bar is moved into the message search layout.
- The Searchbar component is stripped of unused features.
- Some remaining logic that was used to switch main loader components is removed.
- ViewCoordinator.getView gets a "force create" parameter and we no longer preload low-cost views.

NOTE: the option to use a frameless window is available within general settings

Gitlab: #1524 (Frameless Window)
Change-Id: Iec6bdf162cb0335d3ae3d9bd09dd9783991a4a57
This commit is contained in:
Andreas Traczyk 2023-12-21 14:37:32 -05:00
parent 788ecaa496
commit 35482fa92f
52 changed files with 908 additions and 323 deletions

View file

@ -21,7 +21,7 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
cmake_minimum_required(VERSION 3.16)
cmake_minimum_required(VERSION 3.19)
if(APPLE)
project(Jami)
@ -75,6 +75,37 @@ if(NOT MSVC)
set(CMAKE_CXX_FLAGS_DEBUG "-Og -ggdb")
endif()
include(${PROJECT_SOURCE_DIR}/extras/build/cmake/contrib_tools.cmake)
set(EXTRA_PATCHES_DIR ${PROJECT_SOURCE_DIR}/extras/patches)
list(APPEND QWINDOWKIT_OPTIONS
QWINDOWKIT_BUILD_WIDGETS OFF
QWINDOWKIT_INSTALL OFF
QWINDOWKIT_BUILD_STATIC ON
)
if(WIN32)
list(APPEND QWINDOWKIT_PATCHES ${EXTRA_PATCHES_DIR}/0002-workaround-right-margin.patch)
list(APPEND QWINDOWKIT_OPTIONS QWINDOWKIT_ENABLE_WINDOWS_SYSTEM_BORDERS OFF)
endif()
# qmsetup uses the wrong package dir on Fedora at least.
check_redhat_based(IS_REDHAT_BASED)
if(IS_REDHAT_BASED)
list(APPEND QWINDOWKIT_PATCHES ${EXTRA_PATCHES_DIR}/0001-fix-fedora-fc-build.patch)
set(qmsetup_cmake_path ${CMAKE_BINARY_DIR}/_install/lib64/cmake/qmsetup)
endif()
# qwindowkit (frameless window)
add_fetch_content(
TARGET qwindowkit
URL https://github.com/stdware/qwindowkit.git
BRANCH 79b1f3110754f9c21af2d7dacbd07b1a9dbaf6ef
PATCHES ${QWINDOWKIT_PATCHES}
OPTIONS ${QWINDOWKIT_OPTIONS}
)
list(APPEND CLIENT_INCLUDE_DIRS ${QWindowKit_BINARY_DIR}/include)
list(APPEND CLIENT_LIBS QWindowKit::Quick)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
@ -102,7 +133,7 @@ if(QT6_VER AND QT6_PATH)
find_package(QT NAMES Qt6 REQUIRED
PATHS ${QT6_PATH} NO_DEFAULT_PATH)
else()
message(STATUS "Looking for Qt 6" ${CMAKE_PREFIX_PATH})
message(STATUS "Looking for Qt 6 in ${CMAKE_PREFIX_PATH}")
find_package(QT NAMES Qt6 REQUIRED)
endif()
if (${QT_VERSION_MINOR} GREATER_EQUAL ${QT6_MINVER_MINOR})

View file

@ -0,0 +1,84 @@
# Copyright (C) 2024 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
include(FetchContent)
include(CMakeParseArguments)
# Helper function to check if the current distribution is Red Hat-based
function(check_redhat_based IS_REDHAT_BASED)
set(${IS_REDHAT_BASED} FALSE PARENT_SCOPE)
# Check for the existence of /etc/os-release
if(EXISTS "/etc/os-release")
# Read the content of the file
file(READ "/etc/os-release" OS_RELEASE_CONTENT)
# Check if the distribution is Fedora or Red Hat-based
string(REGEX MATCH "ID=fedora|ID_LIKE=\"rhel fedora\"" MATCH_RESULT "${OS_RELEASE_CONTENT}")
if(MATCH_RESULT)
set(${IS_REDHAT_BASED} TRUE PARENT_SCOPE)
message(STATUS "Running on a Red Hat-based distribution (Fedora, RHEL, CentOS, etc.)")
else()
message(STATUS "Not a Red Hat-based distribution")
endif()
else()
message(STATUS "Cannot determine the distribution type: /etc/os-release not found")
endif()
endfunction()
# Helper function to add external content with patches and options.
# Parameters:
# TARGET: Name of the target to create
# URL: URL of the git repository
# BRANCH: Branch to checkout
# PATCHES: List of patch files to apply
# OPTIONS: List of options to set prior to calling FetchContent_MakeAvailable
function(add_fetch_content)
# Parse function arguments
set(oneValueArgs TARGET URL BRANCH)
set(multiValueArgs PATCHES OPTIONS)
cmake_parse_arguments(PARSE_ARGV 0 AFCWP "" "${oneValueArgs}" "${multiValueArgs}")
# Create a string for the patch command
set(patch_cmd "")
# If patches is not empty, start the command with "git apply"
if(NOT "${AFCWP_PATCHES}" STREQUAL "")
set(patch_cmd git apply)
endif()
foreach(patch_file IN LISTS AFCWP_PATCHES)
list(APPEND patch_cmd "${patch_file}")
endforeach()
# Declare the external content
FetchContent_Declare(
${AFCWP_TARGET}
GIT_REPOSITORY ${AFCWP_URL}
GIT_TAG ${AFCWP_BRANCH}
PATCH_COMMAND ${patch_cmd}
UPDATE_DISCONNECTED 1
)
# Apply options
list(LENGTH AFCWP_OPTIONS options_length)
math(EXPR max_idx "${options_length} - 1")
foreach(idx RANGE 0 ${max_idx} 2)
list(GET AFCWP_OPTIONS ${idx} key)
math(EXPR value_idx "${idx} + 1")
list(GET AFCWP_OPTIONS ${value_idx} value)
set(${key} ${value} CACHE STRING "${key}" FORCE)
endforeach()
# Make the content available
FetchContent_MakeAvailable(${AFCWP_TARGET})
endfunction()

View file

@ -66,4 +66,9 @@ RUN apt-get install -y -o Acquire::Retries=10 \
libssl-dev
RUN apt-get install -y pandoc \
googletest \
libgtest-dev
libgtest-dev \
wget
# Install a recent version of CMake
ADD extras/packaging/gnu-linux/scripts/install-cmake.sh /opt/install-cmake.sh
RUN /opt/install-cmake.sh

View file

@ -0,0 +1,25 @@
From 161d28abb6784115ad71fcb6977e112e9d5756d4 Mon Sep 17 00:00:00 2001
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
Date: Tue, 23 Jan 2024 15:38:34 -0500
Subject: [PATCH] fix-fedora-fc-build
---
CMakeLists.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0fb89c8..3a6ad6d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -65,7 +65,7 @@ if(NOT TARGET qmsetup::library)
)
# Find package again
- find_package(qmsetup REQUIRED PATHS ${_package_path})
+ find_package(qmsetup REQUIRED PATHS ${_package_path} ${qmsetup_cmake_path})
# Update import path
set(qmsetup_DIR ${_package_path} CACHE PATH "" FORCE)
--
2.34.1

View file

@ -0,0 +1,34 @@
From ca2be6466c150d1b82a646d97b27df35b45d90f1 Mon Sep 17 00:00:00 2001
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
Date: Tue, 9 Jan 2024 15:25:19 -0500
Subject: [PATCH] workaround right margin
---
src/core/contexts/win32windowcontext.cpp | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/src/core/contexts/win32windowcontext.cpp b/src/core/contexts/win32windowcontext.cpp
index 3f6623e..9ee7752 100644
--- a/src/core/contexts/win32windowcontext.cpp
+++ b/src/core/contexts/win32windowcontext.cpp
@@ -402,6 +402,17 @@ namespace QWK {
return true;
}
}
+
+#if !QWINDOWKIT_CONFIG(ENABLE_WINDOWS_SYSTEM_BORDERS)
+ if (msg->message == WM_MOVE || msg->message == WM_SIZE ||
+ (msg->message == WM_IME_SETCONTEXT && (GetForegroundWindow() == msg->hwnd))) {
+ static const auto flags = SWP_FRAMECHANGED | SWP_NOMOVE |
+ SWP_NOSIZE | SWP_NOZORDER |
+ SWP_NOOWNERZORDER;
+ SetWindowPos(msg->hwnd, NULL, 0, 0, 0, 0, flags);
+ }
+#endif
+
return false;
}
--
2.7.4

View file

@ -0,0 +1,15 @@
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" width="10.88" height="10.88"
viewBox="0 0 10.88 10.88">
<defs>
<style>
.cls-1 {
fill: none;
stroke: white;
stroke-miterlimit: 10;
stroke-width: 1.25px;
}
</style>
</defs>
<line class="cls-1" x1="0.44" y1="0.44" x2="10.44" y2="10.44" />
<line class="cls-1" x1="0.44" y1="10.44" x2="10.44" y2="0.44" />
</svg>

After

Width:  |  Height:  |  Size: 444 B

View file

@ -0,0 +1,11 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1594017175519"
class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1933"
xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
<defs>
<style type="text/css"></style>
</defs>
<path
d="M874.666667 128h-170.666667a21.333333 21.333333 0 0 0 0 42.666667h119.168l-176.917333 176.917333a21.333333 21.333333 0 1 0 30.165333 30.165333L853.333333 200.832V320a21.333333 21.333333 0 0 0 42.666667 0V149.333333a21.333333 21.333333 0 0 0-21.333333-21.333333zM347.584 646.250667L170.666667 823.168V704a21.333333 21.333333 0 0 0-42.666667 0v170.666667a21.333333 21.333333 0 0 0 21.333333 21.333333h170.666667a21.333333 21.333333 0 0 0 0-42.666667H200.832l176.917333-176.917333a21.333333 21.333333 0 0 0-30.165333-30.165333zM874.666667 682.666667a21.333333 21.333333 0 0 0-21.333334 21.333333v119.168l-176.917333-176.917333a21.333333 21.333333 0 0 0-30.165333 30.165333L823.168 853.333333H704a21.333333 21.333333 0 0 0 0 42.666667h170.666667a21.333333 21.333333 0 0 0 21.333333-21.333333v-170.666667a21.333333 21.333333 0 0 0-21.333333-21.333333zM200.832 170.666667H320a21.333333 21.333333 0 0 0 0-42.666667H149.333333a21.333333 21.333333 0 0 0-21.333333 21.333333v170.666667a21.333333 21.333333 0 0 0 42.666667 0V200.832l176.917333 176.917333a21.333333 21.333333 0 0 0 30.165333-30.165333z"
fill="#ffffff" p-id="1934"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,12 @@
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<defs>
<style>
.cls-1 {
fill: none;
stroke: white;
stroke-miterlimit: 10;
}
</style>
</defs>
<rect class="cls-1" x="0.5" y="0.5" width="9" height="9" />
</svg>

After

Width:  |  Height:  |  Size: 328 B

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 10 10" style="enable-background:new 0 0 10 10;" xml:space="preserve">
<style type="text/css">
.st0 {
fill: white;
}
</style>
<rect y="4.5" class="st0" width="10" height="1" />
</svg>

After

Width:  |  Height:  |  Size: 467 B

View file

@ -0,0 +1,16 @@
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
<defs>
<style>
.cls-1 {
fill: none;
stroke: white;
stroke-miterlimit: 10;
}
</style>
</defs>
<rect class="cls-1" x="0.5" y="2.5" width="9" height="9" />
<line class="cls-1" x1="2.5" y1="2.5" x2="2.5" y2="0.5" />
<line class="cls-1" x1="12" y1="0.5" x2="2" y2="0.5" />
<line class="cls-1" x1="11.5" y1="10" x2="11.5" />
<line class="cls-1" x1="10" y1="9.5" x2="12" y2="9.5" />
</svg>

After

Width:  |  Height:  |  Size: 559 B

View file

@ -73,17 +73,26 @@ QtObject {
function saveWindowSettings() {
// If closed-to-tray or minimized or fullscreen, save the cached windowedVisibility
// value instead.
if (isHidden || isFullScreen) {
AppSettingsManager.setValue(Settings.WindowState, priv.windowedVisibility)
} else {
AppSettingsManager.setValue(Settings.WindowState, visibility)
}
const visibilityToSave = isHidden || isFullScreen ? priv.windowedVisibility : visibility;
// Likewise, don't save fullscreen geometry.
const geometry = isFullScreen ?
priv.windowedGeometry :
Qt.rect(appWindow.x, appWindow.y,
appWindow.width, appWindow.height)
appWindow.width, appWindow.height);
// QWK: Account for the frameless window's offset.
if (appWindow.useFrameless) {
if (Qt.platform.os.toString() !== "osx") {
// Add [7, 30, 0, 0] on Windows and GNU/Linux.
geometry.x += 7;
geometry.y += 30;
}
}
console.debug("Saving window: " + JSON.stringify(geometry) + " " + visibilityToSave);
AppSettingsManager.setValue(Settings.WindowState, visibilityToSave)
AppSettingsManager.setValue(Settings.WindowGeometry, geometry)
}
@ -111,6 +120,8 @@ QtObject {
const visibilityStr = AppSettingsManager.getValue(Settings.WindowState)
var visibilitySetting = parseInt(visibilityStr)
console.debug("Restoring window: " + JSON.stringify(geometry) + " " + visibilitySetting)
// We should never restore a hidden or fullscreen state here. Default to normal
// windowed state in such a case. This shouldn't happen.
if (visibilitySetting === Window.Hidden || visibilitySetting === Window.FullScreen) {

View file

@ -24,29 +24,30 @@ import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import net.jami.Models 1.1
import net.jami.Adapters 1.1
import net.jami.Enums 1.1
import net.jami.Helpers 1.1
import net.jami.Constants 1.1
import "mainview"
import "mainview/components"
import "wizardview"
import "commoncomponents"
import QWindowKit
ApplicationWindow {
id: root
id: appWindow
property bool isRTL: UtilsAdapter.isRTL
LayoutMirroring.enabled: isRTL
LayoutMirroring.childrenInherit: isRTL
enum LoadedSource {
MainView,
AccountMigrationView,
None
}
// This needs to be set from the start.
readonly property bool useFrameless: UtilsAdapter.getAppValue(Settings.Key.UseFramelessWindow)
onActiveFocusItemChanged: {
focusOverlay.margin = -5;
@ -70,7 +71,7 @@ ApplicationWindow {
sourceComponent: GenericErrorsRow {
id: genericError
text: CurrentAccount.enabled ? JamiStrings.noNetworkConnectivity : JamiStrings.disabledAccount
height: visible? JamiTheme.chatViewHeaderPreferredHeight : 0
height: visible? JamiTheme.qwkTitleBarHeight : 0
}
}
@ -88,37 +89,31 @@ ApplicationWindow {
border.color: JamiTheme.tintedBlue
}
property ApplicationWindow appWindow: root
property LayoutManager layoutManager: LayoutManager {
appContainer: appContainer
}
property ViewManager viewManager: ViewManager {
}
property ViewCoordinator viewCoordinator: ViewCoordinator {
viewManager: root.viewManager
// Used to manage full screen mode and save/restore window geometry.
LayoutManager {
id: layoutManager
appContainer: fullscreenContainer
}
// Used to manage dynamic view loading and unloading.
ViewManager {
id: viewManager
}
// Used to manage the view stack and the current view.
ViewCoordinator {
id: viewCoordinator
}
// Used to prevent the window from being visible until the
// window geometry has been restored and the view stack has
// been loaded.
property bool windowSettingsLoaded: false
// This setting can be used to block a loading Jami instance
// from showNormal() and showMaximized() when starting minimized.
property bool allowVisibleWindow: true
function checkLoadedSource() {
var sourceString = mainApplicationLoader.source.toString();
if (sourceString === JamiQmlUtils.mainViewLoadPath)
return MainApplicationWindow.LoadedSource.MainView;
return MainApplicationWindow.LoadedSource.None;
}
function startClient() {
setMainLoaderSource(JamiQmlUtils.mainViewLoadPath);
}
function setMainLoaderSource(source) {
if (checkLoadedSource() === MainApplicationWindow.LoadedSource.MainView) {
cleanupMainView();
}
mainApplicationLoader.setSource(source);
}
function cleanupMainView() {
// Save the main view window size if loading anything else.
layoutManager.saveWindowSettings();
@ -139,88 +134,148 @@ ApplicationWindow {
title: JamiStrings.appTitle
visible: mainApplicationLoader.status === Loader.Ready && windowSettingsLoaded && allowVisibleWindow
visible: mainViewLoader.status === Loader.Ready && windowSettingsLoaded && allowVisibleWindow
// To facilitate reparenting of the callview during
// fullscreen mode, we need QQuickItem based object.
Item {
id: appContainer
Connections {
id: connectionMigrationEnded
anchors.fill: parent
target: CurrentAccountToMigrate
function onAccountNeedsMigration(accountId) {
viewCoordinator.present("AccountMigrationView");
}
function onAllMigrationsFinished() {
viewCoordinator.dismiss("AccountMigrationView");
viewCoordinator.present("WelcomePage");
}
}
function initMainView(view) {
console.info("Initializing main view");
// Main window, load any valid app settings, and allow the
// layoutManager to handle as much as possible.
layoutManager.restoreWindowSettings();
// QWK: setup
if (useFrameless) {
windowAgent.setTitleBar(titleBar);
// Now register the system buttons (non-macOS).
if (sysBtnsLoader.item) {
const sysBtns = sysBtnsLoader.item;
windowAgent.setSystemButton(WindowAgent.Minimize, sysBtns.minButton);
windowAgent.setSystemButton(WindowAgent.Maximize, sysBtns.maxButton);
windowAgent.setSystemButton(WindowAgent.Close, sysBtns.closeButton);
}
}
// Set the viewCoordinator's root item.
viewCoordinator.init(view);
// Navigate to something.
if (UtilsAdapter.getAccountListSize() > 0) {
// Already have an account.
if (CurrentAccountToMigrate.accountToMigrateListSize > 0)
// Do we need to migrate any accounts?
viewCoordinator.present("AccountMigrationView");
else
// Okay now just start the client normally.
viewCoordinator.present("WelcomePage");
} else {
// No account, so start the wizard.
viewCoordinator.present("WizardView");
}
// Set up the event filter for macOS.
if (Qt.platform.os.toString() === "osx") {
MainApplication.setEventFilter();
}
// Quiet check for updates on start if set to.
if (Qt.platform.os.toString() === "windows") {
if (UtilsAdapter.getAppValue(Settings.AutoUpdate)) {
AppVersionManager.checkForUpdates(true);
AppVersionManager.setAutoUpdateCheck(true);
}
}
// Handle a start URI if set as start option.
MainApplication.handleUriAction();
// This will allow visible to become true if not starting minimized.
windowSettingsLoaded = true;
}
Component.onCompleted: {
// QWK: setup
if (useFrameless) {
windowAgent.setup(appWindow);
}
mainViewLoader.active = true;
// Dbus error handler for Linux.
if (Qt.platform.os.toString() !== "windows" && Qt.platform.os.toString() !== "osx")
DBusErrorHandler.setActive(true);
}
Loader {
id: mainApplicationLoader
id: mainViewLoader
active: false
source: "qrc:/mainview/MainView.qml"
anchors.fill: parent
z: -1
onLoaded: initMainView(item)
}
asynchronous: true
visible: status == Loader.Ready
// Use this as a parent for fullscreen items.
Item {
id: fullscreenContainer
anchors.fill: parent
}
Connections {
id: connectionMigrationEnded
// QWK: Provide spacing for widgets that may be occluded by the system buttons.
QtObject {
id: qwkSystemButtonSpacing
readonly property bool isMacOS: Qt.platform.os.toString() === "osx"
readonly property bool isFullscreen: layoutManager.isFullScreen
// macOS buttons are on the left.
readonly property real left: useFrameless && isMacOS && viewCoordinator.isInSinglePaneMode ? 80 : 0
// Windows and Linux buttons are on the right.
readonly property real right: useFrameless && !isMacOS && !isFullscreen ? sysBtnsLoader.width + 24 : 0
}
target: CurrentAccountToMigrate
function onAccountNeedsMigration(accountId) {
viewCoordinator.present("AccountMigrationView");
}
function onAllMigrationsFinished() {
viewCoordinator.dismiss("AccountMigrationView");
startClient();
}
// QWK: Window Title bar
Item {
id: titleBar
height: JamiTheme.qwkTitleBarHeight
anchors {
top: parent.top
right: parent.right
left: parent.left
}
// Set `visible = false` when loading a new QML file.
onSourceChanged: windowSettingsLoaded = false
onLoaded: {
if (UtilsAdapter.getAccountListSize() === 0) {
layoutManager.restoreWindowSettings();
if (!viewCoordinator.rootView)
// Set the viewCoordinator's root item.
viewCoordinator.init(item);
viewCoordinator.present("WizardView");
} else {
// Main window, load any valid app settings, and allow the
// layoutManager to handle as much as possible.
layoutManager.restoreWindowSettings();
// Present the welcome view once the viewCoordinator is setup.
viewCoordinator.initialized.connect(function () {
viewCoordinator.preload("SidePanel");
viewCoordinator.preload("SettingsSidePanel");
viewCoordinator.present("WelcomePage");
viewCoordinator.preload("ConversationView");
});
if (!viewCoordinator.rootView)
// Set the viewCoordinator's root item.
viewCoordinator.init(item);
if (CurrentAccountToMigrate.accountToMigrateListSize > 0)
viewCoordinator.present("AccountMigrationView");
// On Windows and Linux, use custom system buttons.
Loader {
id: sysBtnsLoader
active: Qt.platform.os.toString() !== "osx" && useFrameless
height: titleBar.height
anchors {
top: parent.top
right: parent.right
// Note: leave these margins, they prevent image scaling artifacts
topMargin: 1
rightMargin: 1
}
if (Qt.platform.os.toString() === "osx") {
MainApplication.setEventFilter();
}
// This will trigger `visible = true`.
windowSettingsLoaded = true;
// Quiet check for updates on start if set to.
if (Qt.platform.os.toString() === "windows") {
if (UtilsAdapter.getAppValue(Settings.AutoUpdate)) {
AppVersionManager.checkForUpdates(true);
AppVersionManager.setAutoUpdateCheck(true);
}
}
// Handle a start URI if set as start option.
MainApplication.handleUriAction();
source: "qrc:/commoncomponents/QWKSystemButtonGroup.qml"
}
}
// QWK: Main interop component.
WindowAgent {
id: windowAgent
}
Connections {
target: LRCInstance
@ -339,11 +394,5 @@ ApplicationWindow {
}
}
onClosing: root.close()
Component.onCompleted: {
startClient();
if (Qt.platform.os.toString() !== "windows" && Qt.platform.os.toString() !== "osx")
DBusErrorHandler.setActive(true);
}
onClosing: appWindow.close()
}

View file

@ -24,14 +24,6 @@ import "commoncomponents"
QtObject {
id: root
required property QtObject viewManager
signal initialized
function requestAppWindowWizardView() {
viewCoordinator.present("WizardView");
}
// A map of view names to file paths for QML files that define each view.
property variant resources: {
"SidePanel": "mainview/components/SidePanel.qml",
@ -47,12 +39,44 @@ QtObject {
// The `main` view of the application window.
property StackView rootView
property var currentViewName: rootView && rootView.currentItem && rootView.currentItem.objectName || null
readonly property Item currentView: rootView && rootView.currentItem || null
readonly property var currentViewName: currentView && currentView.objectName || null
readonly property bool isDualPane: currentView && currentView instanceof DualPaneView
readonly property bool isInSinglePaneMode: !isDualPane || currentView.isSinglePane
readonly property bool isRTL: Qt.application.layoutDirection === Qt.RightToLeft
// A list of the current visible views. This could be a single view or two views in
// dual pane mode. The list is ordered [minor, major] where major is the view on the
// right side when not in RTL and should represent the main or content-type view.
readonly property var visibleViews: {
if (!currentView)
return []
if (isDualPane) {
if (isInSinglePaneMode)
return [currentView.rightPaneItem]
return [currentView.leftPaneItem, currentView.rightPaneItem]
}
return [currentView]
}
// Aggregate this info and expose it as a single string for convenience.
// JSON indented by 2 spaces.
readonly property string currentViewInfo: {
var info = {
currentViewName: currentViewName,
isDualPane: isDualPane,
isInSinglePaneMode: isInSinglePaneMode,
visibleViews: visibleViews.map(function(view) {
return view && view.objectName || null;
}),
visibleViewWidths: visibleViews.map(function(view) {
return view && view.width || null;
}),
};
return JSON.stringify(info, null, 2);
}
function init(mainStackView) {
rootView = Qt.createQmlObject(`import QtQuick; import QtQuick.Controls
StackView { anchors.fill: parent }`, mainStackView);
initialized();
}
function deinit() {
@ -171,6 +195,8 @@ QtObject {
var objectName = view ? view.objectName : obj.objectName;
if (!viewManager.destroyView(resources[objectName])) {
print("could not destroy view:", objectName);
} else {
print("destroyed view:", objectName);
}
} else
view.dismissed();
@ -197,8 +223,20 @@ QtObject {
}
}
function getView(viewName) {
return viewManager.getView(viewName);
function getView(viewName, forceCreate = false) {
// If the view is already loaded, return it.
var view = viewManager.getView(viewName);
if (view)
return view;
if (!forceCreate)
return null;
// Otherwise, create it.
view = viewManager.createView(resources[viewName], null);
if (!view) {
console.log("Failed to load view: " + viewName);
return null;
}
return view;
}
// Load a view without presenting it.

View file

@ -33,6 +33,14 @@
extern const QString defaultDownloadPath;
// clang-format off
// Define USE_FRAMELESS_WINDOW_DEFAULT based on the platform
#ifdef Q_OS_LINUX
#define USE_FRAMELESS_WINDOW_DEFAULT false
#else
#define USE_FRAMELESS_WINDOW_DEFAULT true
#endif
// Common key-value pairs for both APPSTORE and non-APPSTORE builds
#define COMMON_KEYS \
X(MinimizeOnClose, false) \
@ -66,7 +74,8 @@ extern const QString defaultDownloadPath;
X(ChatViewEnterIsNewLine, false) \
X(ShowSendOption, false) \
X(EnablePtt, false) \
X(PttKeys, 32)
X(PttKeys, 32) \
X(UseFramelessWindow, USE_FRAMELESS_WINDOW_DEFAULT)
#ifdef APPSTORE
#define KEYS COMMON_KEYS
#else

View file

@ -35,10 +35,12 @@ Rectangle {
signal dismissed
Component.onCompleted: {
console.debug("Created", objectName);
if (managed)
presented();
}
Component.onDestruction: {
console.debug("Destroyed", objectName);
if (managed)
dismissed();
}

View file

@ -0,0 +1,82 @@
/*
* Copyright (C) 2024 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 Qt5Compat.GraphicalEffects
import net.jami.Adapters 1.1
import net.jami.Constants 1.1
Button {
id: control
width: height * 0.9156626506024096
leftInset: 0
topInset: 0
rightInset: 0
bottomInset: 0
padding: 0
function calculateLuminance(color) {
return 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
}
property alias source: image.source
contentItem: Item {
Image {
id: image
anchors.centerIn: parent
mipmap: true
width: 12
height: 12
layer.enabled: true
layer.effect: ColorOverlay {
color: {
// We may force this color to be white, when the background is dark.
var backgroundIsDark = calculateLuminance(control.background.color) > 0.25;
// This includes when we are in a call (which has a dark background).
backgroundIsDark = backgroundIsDark || CurrentConversation.hasCall;
return backgroundIsDark ? "white" : JamiTheme.primaryForegroundColor;
}
}
}
}
property color baseColor: {
// Avoid transparent if the background is dark i.e. in a call.
if (CurrentConversation.hasCall)
return Qt.rgba(1, 1, 1, 0.5);
return JamiTheme.darkTheme ? Qt.rgba(1, 1, 1, 0.15) : Qt.rgba(0, 0, 0, 0.15);
}
readonly property color pressedColor: {
const darker = Qt.darker(baseColor, 1.3);
return Qt.rgba(darker.r, darker.g, darker.b, baseColor.a * 1.3);
}
background: Rectangle {
color: {
if (!control.enabled)
return "gray";
if (control.pressed)
return control.pressedColor;
if (control.hovered)
return control.baseColor;
return "transparent";
}
Behavior on color { ColorAnimation { duration: 100 } }
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (C) 2024 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
Item {
// Wait for it's parent to be created, then set it to be hit test visible.
// This avoids having to edit Component.onCompleted of the parent.
// Note: this is experimental. TBD if this is a good way to do this.
// This technique makes it clear and simple to implement, but may have
// side effects beyond just adding a dummy item component.
// Best alternatives:
// - Wrap the parent in a custom component that is hit test visible.
// - Edit the parent's Component.onCompleted to set it to be hit test visible.
Component.onCompleted: Qt.callLater(function() {
if (appWindow && appWindow.useFrameless)
windowAgent.setHitTestVisible(parent, true);
});
// Likewise, wait for it's parent to be destroyed, then set it to be hit test invisible.
Component.onDestruction: {
if (appWindow && appWindow.useFrameless)
windowAgent.setHitTestVisible(parent, false);
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (C) 2024 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 net.jami.Constants 1.1
import QWindowKit
Row {
id: root
property alias minButton: minButton
property alias maxButton: maxButton
property alias closeButton: closeButton
component SystemButton : QWKButton {
height: parent.height
}
visible: appWindow.visibility !== Window.FullScreen
SystemButton {
id: minButton
source: JamiResources.window_bar_minimize_svg
onClicked: appWindow.showMinimized()
}
SystemButton {
id: maxButton
source: appWindow.visibility === Window.Maximized ?
JamiResources.window_bar_restore_svg :
JamiResources.window_bar_maximize_svg
onClicked: appWindow.visibility === Window.Maximized ?
appWindow.showNormal() :
appWindow.showMaximized()
}
SystemButton {
id: closeButton
source: JamiResources.window_bar_close_svg
baseColor: "#e81123"
onClicked: appWindow.close()
}
}

View file

@ -1,10 +1,53 @@
import QtQuick
/*
* Copyright (C) 2022-2024 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/>.
*/
Rectangle {
import QtQuick
import QtQuick.Controls
Page {
id: root
anchors.fill: parent
property color color: "transparent"
// QWK: Title bar spacing for macOS and single pane mode.
// Not using topMargin here on purpose, to make is simple to
// keep the theme coloring without wrapping components that
// derive from SidePanelBase.
header: Rectangle {
id: titleBarSpacer
height: {
if (!appWindow.useFrameless)
return 0;
var extraHeight = 0;
if (Qt.platform.os.toString() === "osx")
extraHeight = 24;
else if (viewCoordinator.isInSinglePaneMode)
extraHeight = titleBar.height;
return extraHeight;
}
color: root.color
}
background: Rectangle {
color: root.color
}
// Override these if needed.
property var select: function () {}
property var deselect: function () {}

View file

@ -24,9 +24,6 @@ import net.jami.Enums 1.1
Item {
property string qmlFilePrefix: "file:/"
readonly property string mainViewLoadPath: "qrc:/mainview/MainView.qml"
readonly property string wizardViewLoadPath: "qrc:/wizardview/WizardView.qml"
readonly property string base64StringTitle: "data:image/png;base64,"
property var accountCreationInputParaObject: ({})
@ -81,8 +78,7 @@ Item {
function onDonationCampaignSettingsChanged() {
// Changing any of the donation campaign settings will trigger a recompute
// of the banner visibility.
updateIsDonationBannerVisible();
}
updateIsDonationBannerVisible(); }
}
function updateIsDonationBannerVisible() {

View file

@ -477,6 +477,7 @@ Item {
property string enableNotifications: qsTr("Enable notifications")
property string showNotifications: qsTr("Show notifications")
property string keepMinimized: qsTr("Minimize on close")
property string useNativeWindowFrame: qsTr("Use native window frame (requires restart)")
property string tipRunStartup: qsTr("Run at system startup")
property string runStartup: qsTr("Launch at startup")
property string downloadFolder: qsTr("Choose download directory")

View file

@ -474,7 +474,6 @@ Item {
// MessageWebView
property real chatViewHairLineSize: 1
property real chatViewMaximumWidth: 900
property real chatViewHeaderPreferredHeight: 50
property real chatViewFooterPreferredHeight: 35
property real chatViewFooterMaximumHeight: 315
property real chatViewFooterRowSpacing: 4
@ -710,4 +709,7 @@ Item {
property color chatSettingButtonBackgroundColor: darkTheme ? "#303030" : "#F0EFEF"
property color chatSettingButtonBorderColor: darkTheme ? "#03B9E9" : "#005699"
property color chatSettingButtonTextColor: textColor
// QWK
property real qwkTitleBarHeight: 50
}

View file

@ -27,6 +27,8 @@
#include "systemtray.h"
#include "videoprovider.h"
#include <QWKQuick/qwkquickglobal.h>
#include <QAction>
#include <QCommandLineParser>
#include <QCoreApplication>
@ -138,7 +140,6 @@ ScreenInfo::onPhysicalDotsPerInchChanged()
MainApplication::MainApplication(int& argc, char** argv)
: QApplication(argc, argv)
, isCleanupped(false)
{
const char* qtVersion = qVersion();
if (strncmp(qtVersion, QT_VERSION_STR, strnlen(qtVersion, sizeof qtVersion)) != 0) {
@ -166,8 +167,6 @@ MainApplication::MainApplication(int& argc, char** argv)
// the logging features.
qInstallMessageHandler(messageHandler);
QObject::connect(this, &QApplication::aboutToQuit, this, &MainApplication::cleanup);
qCInfo(app_) << "Using Qt runtime version:" << qtVersion;
}
@ -184,6 +183,8 @@ MainApplication::init()
// performing unnecessary tasks, like initializing the webengine.
engine_.reset(new QQmlApplicationEngine(this));
QWK::registerTypes(engine_.get());
connectivityMonitor_ = new ConnectivityMonitor(this);
settingsManager_ = new AppSettingsManager(this);
systemTray_ = new SystemTray(settingsManager_, this);
@ -466,17 +467,6 @@ MainApplication::initSystray()
systemTray_->show();
}
void
MainApplication::cleanup()
{
// In Qt 6.5, QApplication::exit(0) will signal aboutToQuit, and aboutToQuit is connected to cleanup
// TODO: delete cleanup.
if (!isCleanupped) {
isCleanupped = true;
QApplication::exit(0);
}
}
void
MainApplication::setEventFilter()
{

View file

@ -108,7 +108,6 @@ private:
void setApplicationFont();
void initQmlLayer();
void initSystray();
void cleanup();
private:
std::map<Option, QVariant> runOptions_;
@ -123,6 +122,4 @@ private:
AppSettingsManager* settingsManager_;
ScreenInfo screenInfo_;
bool isCleanupped;
};

View file

@ -45,9 +45,11 @@ ListSelectionView {
color: JamiTheme.transparentColor
leftPaneItem: viewCoordinator.getView("SidePanel")
leftPaneItem: viewCoordinator.getView("SidePanel", true)
rightPaneItem: StackLayout {
objectName: "ConversationLayout"
currentIndex: !CurrentConversation.hasCall ? 0 : 1
onCurrentIndexChanged: chatView.parent = currentIndex === 1 ? callStackView.chatViewContainer : chatViewContainer

View file

@ -36,13 +36,8 @@ import "js/keyboardshortcuttablecreation.js" as KeyboardShortcutTableCreation
Rectangle {
id: mainView
objectName: "mainView"
// To calculate tab bar bottom border hidden rect left margin.
property int tabBarLeftMargin: 8
property int tabButtonShrinkSize: 8
property string currentConvId: CurrentConversation.id
onCurrentConvIdChanged: {
if (currentConvId !== '') {

View file

@ -115,8 +115,8 @@ Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.preferredHeight: JamiTheme.chatViewHeaderPreferredHeight
Layout.maximumHeight: JamiTheme.chatViewHeaderPreferredHeight
Layout.preferredHeight: JamiTheme.qwkTitleBarHeight
Layout.maximumHeight: JamiTheme.qwkTitleBarHeight
Layout.minimumWidth: JamiTheme.mainViewPaneMinWidth
DropArea {
@ -180,14 +180,14 @@ Rectangle {
ConversationErrorsRow {
id: errorRect
Layout.fillWidth: true
Layout.preferredHeight: JamiTheme.chatViewHeaderPreferredHeight
Layout.preferredHeight: JamiTheme.qwkTitleBarHeight
visible: false
}
NotificationArea {
id: notificationArea
Layout.fillWidth: true
Layout.preferredHeight: JamiTheme.chatViewHeaderPreferredHeight
Layout.preferredHeight: JamiTheme.qwkTitleBarHeight
visible: CurrentConversation.activeCalls.length > 0 && !root.inCallView
}

View file

@ -46,7 +46,7 @@ Rectangle {
}
}
property bool interactionButtonsVisibility: {
readonly property bool interactionButtonsVisibility: {
if (CurrentConversation.inCall)
return false;
if (LRCInstance.currentAccountType === Profile.Type.SIP)
@ -74,16 +74,17 @@ Rectangle {
id: messagingHeaderRectRowLayout
anchors.fill: parent
anchors.rightMargin: 8
// QWK: spacing
anchors.leftMargin: qwkSystemButtonSpacing.left
anchors.rightMargin: 10 + qwkSystemButtonSpacing.right
spacing: 16
JamiPushButton {
JamiPushButton { QWKSetParentHitTestVisible {}
id: backToWelcomeViewButton
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.leftMargin: 8
//preferredSize: 24
mirror: UtilsAdapter.isRTL
source: JamiResources.back_24dp_svg
@ -106,10 +107,11 @@ Rectangle {
color: JamiTheme.transparentColor
ColumnLayout {
ColumnLayout { QWKSetParentHitTestVisible {}
id: userNameOrIdColumnLayout
objectName: "userNameOrIdColumnLayout"
anchors.fill: parent
height: parent.height
spacing: 0
@ -144,63 +146,30 @@ Rectangle {
}
}
Searchbar {
id: rowSearchBar
reductionEnabled: true
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Layout.preferredHeight: 30
Layout.preferredWidth: 40 + (isOpen ? JamiTheme.searchbarSize : 0)
colorSearchBar: JamiTheme.backgroundColor
hoverButtonRadius: JamiTheme.chatViewHeaderButtonRadius
Behavior on Layout.preferredWidth {
NumberAnimation {
duration: 150
}
}
visible: root.swarmDetailsVisibility
onSearchBarTextChanged: function (text) {
MessagesAdapter.searchbarPrompt = text;
}
onSearchClicked: extrasPanel.switchToPanel(ChatView.MessagesResearchPanel)
Shortcut {
sequence: "Ctrl+Shift+F"
context: Qt.ApplicationShortcut
enabled: rowSearchBar.visible
onActivated: {
rowSearchBar.openSearchBar();
}
}
}
JamiPushButton {
JamiPushButton { QWKSetParentHitTestVisible {}
id: startAAudioCallButton
visible: interactionButtonsVisibility && (!addMemberVisibility || UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm))
visible: interactionButtonsVisibility &&
(!addMemberVisibility || UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm))
source: JamiResources.place_audiocall_24dp_svg
toolTipText: JamiStrings.placeAudioCall
onClicked: CallAdapter.placeAudioOnlyCall()
}
JamiPushButton {
JamiPushButton { QWKSetParentHitTestVisible {}
id: startAVideoCallButton
visible: CurrentAccount.videoEnabled_Video && interactionButtonsVisibility && (!addMemberVisibility || UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm))
visible: interactionButtonsVisibility &&
CurrentAccount.videoEnabled_Video &&
(!addMemberVisibility || UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm))
source: JamiResources.videocam_24dp_svg
toolTipText: JamiStrings.placeVideoCall
onClicked: CallAdapter.placeCall()
}
JamiPushButton {
JamiPushButton { QWKSetParentHitTestVisible {}
id: addParticipantsButton
checkable: true
@ -212,7 +181,7 @@ Rectangle {
onClicked: extrasPanel.switchToPanel(ChatView.AddMemberPanel)
}
JamiPushButton {
JamiPushButton { QWKSetParentHitTestVisible {}
id: selectPluginButton
visible: PluginAdapter.chatHandlersListCount && interactionButtonsVisibility
@ -222,7 +191,7 @@ Rectangle {
onClicked: pluginSelector()
}
JamiPushButton {
JamiPushButton { QWKSetParentHitTestVisible {}
id: sendContactRequestButton
objectName: "sendContactRequestButton"
@ -230,16 +199,39 @@ Rectangle {
source: JamiResources.add_people_24dp_svg
toolTipText: JamiStrings.addToConversations
onClicked: CurrentConversation.isBanned ? MessagesAdapter.unbanConversation(CurrentConversation.id) : MessagesAdapter.sendConversationRequest()
onClicked: CurrentConversation.isBanned ?
MessagesAdapter.unbanConversation(CurrentConversation.id) :
MessagesAdapter.sendConversationRequest()
}
JamiPushButton {
JamiPushButton { QWKSetParentHitTestVisible {}
id: searchMessagesButton
objectName: "searchMessagesButton"
checkable: true
checked: extrasPanel.isOpen(ChatView.MessagesResearchPanel)
visible: root.swarmDetailsVisibility
source: JamiResources.ic_baseline_search_24dp_svg
toolTipText: JamiStrings.search
onClicked: extrasPanel.switchToPanel(ChatView.MessagesResearchPanel)
Shortcut {
sequence: "Ctrl+Shift+F"
context: Qt.ApplicationShortcut
enabled: parent.visible
onActivated: extrasPanel.switchToPanel(ChatView.MessagesResearchPanel)
}
}
JamiPushButton { QWKSetParentHitTestVisible {}
id: detailsButton
objectName: "detailsButton"
checkable: true
checked: extrasPanel.isOpen(ChatView.SwarmDetailsPanel)
visible: interactionButtonsVisibility && (swarmDetailsVisibility || LRCInstance.currentAccountType === Profile.Type.SIP) // TODO if SIP not a request
visible: interactionButtonsVisibility &&
(swarmDetailsVisibility || LRCInstance.currentAccountType === Profile.Type.SIP) // TODO if SIP not a request
source: JamiResources.swarm_details_panel_svg
toolTipText: JamiStrings.details

View file

@ -127,9 +127,12 @@ Item {
id: overlayUpperPartRect
anchors.top: parent.top
width: parent.width
height: 50
anchors.left: parent.left
anchors.right: parent.right
// QWK: spacing
anchors.leftMargin: qwkSystemButtonSpacing.left
anchors.rightMargin: qwkSystemButtonSpacing.right
RowLayout {
anchors.fill: parent

View file

@ -26,13 +26,33 @@ import net.jami.Constants 1.1
import "../../commoncomponents"
import "../../settingsview/components"
Rectangle {
Page {
id: root
color: JamiTheme.chatviewBgColor
header: Item {
height: 45
Searchbar {
onVisibleChanged: {
if (visible) {
clearText();
forceActiveFocus();
}
}
anchors.fill: parent
anchors.margins: 5
onSearchBarTextChanged: function (text) {
MessagesAdapter.searchbarPrompt = text;
}
}
}
background: Rectangle {
color: JamiTheme.backgroundColor
}
ColumnLayout {
anchors.fill: parent
anchors.topMargin: 10
TabBar {
id: researchTabBar
@ -88,7 +108,7 @@ Rectangle {
Rectangle {
id: view
color: JamiTheme.chatviewBgColor
color: JamiTheme.backgroundColor
Layout.fillWidth: true
Layout.fillHeight: true

View file

@ -41,9 +41,11 @@ DualPaneView {
splitViewStateKey: "Main"
inhibits: ["ConversationView"]
leftPaneItem: viewCoordinator.getView("SidePanel")
leftPaneItem: viewCoordinator.getView("SidePanel", true)
rightPaneItem: Rectangle {
id: root
objectName: "NewSwarmLayout"
color: JamiTheme.chatviewBgColor
anchors.fill: parent

View file

@ -26,31 +26,19 @@ Rectangle {
signal searchBarTextChanged(string text)
signal returnPressedWhileSearching
signal searchClicked
property bool reductionEnabled: false
property alias textContent: textArea.text
property alias placeHolderText: textArea.placeholderText
property real hoverButtonRadius: JamiTheme.chatViewHeaderButtonRadius
property var colorSearchBar: JamiTheme.secondaryBackgroundColor
property string currentConversationId: CurrentConversation.id
property bool isOpen: reductionEnabled ? extrasPanel.isOpen(ChatView.MessagesResearchPanel) : true
onIsOpenChanged: {
if (isOpen)
textArea.forceActiveFocus();
}
function clearText() {
textArea.clear();
textArea.forceActiveFocus();
}
radius: JamiTheme.primaryRadius
color: isOpen ? colorSearchBar : "transparent"
color: JamiTheme.secondaryBackgroundColor
onFocusChanged: {
if (focus) {
@ -64,32 +52,14 @@ Rectangle {
lineEditObj: textArea
}
PushButton {
ResponsiveImage {
id: startSearch
anchors.verticalCenter: root.verticalCenter
anchors.left: root.left
anchors.leftMargin: 10
hoverEnabled: reductionEnabled
enabled: reductionEnabled
radius: hoverButtonRadius
hoveredColor: JamiTheme.hoveredButtonColor
source: JamiResources.ic_baseline_search_24dp_svg
toolTipText: JamiStrings.search
normalColor: JamiTheme.primaryBackgroundColor
imageColor: {
if (reductionEnabled) {
if (hovered) {
JamiTheme.chatviewButtonColor;
} else {
JamiTheme.chatViewFooterImgColor;
}
} else {
JamiTheme.chatviewButtonColor;
}
}
onClicked: root.searchClicked()
color: JamiTheme.chatviewButtonColor
}
Rectangle {
@ -100,21 +70,7 @@ Rectangle {
anchors.right: root.right
anchors.verticalCenter: root.verticalCenter
color: "transparent"
opacity: isOpen
visible: opacity
Behavior on opacity {
NumberAnimation {
duration: 150
}
}
width: isOpen ? JamiTheme.searchbarSize : 0
Behavior on width {
NumberAnimation {
duration: 150
}
}
width: JamiTheme.searchbarSize
TextField {
id: textArea

View file

@ -173,7 +173,7 @@ SidePanelBase {
color: JamiTheme.backgroundColor
}
header: AccountComboBox {
header: AccountComboBox { QWKSetParentHitTestVisible {}
id: accountComboBox
Shortcut {
sequence: "Ctrl+J"

View file

@ -35,7 +35,7 @@ ListSelectionView {
color: JamiTheme.secondaryBackgroundColor
onPresented: LRCInstance.deselectConversation()
leftPaneItem: viewCoordinator.getView("SidePanel")
leftPaneItem: viewCoordinator.getView("SidePanel", true)
property variant uiCustomization: CurrentAccount.uiCustomization
@ -120,6 +120,8 @@ ListSelectionView {
rightPaneItem: JamiFlickable {
id: root
objectName: "WelcomeLayout"
anchors.fill: parent
property int thresholdSize: 700
property int thresholdHeight: 570

View file

@ -263,7 +263,13 @@ SidePanelBase {
background: null
header: AccountComboBox {
header: AccountComboBox { QWKSetParentHitTestVisible {}
id: accountComboBox
Shortcut {
sequence: "Ctrl+J"
context: Qt.ApplicationShortcut
onActivated: accountComboBox.togglePopup()
}
}
ListView {

View file

@ -56,7 +56,7 @@ ListSelectionView {
splitViewStateKey: "Main"
inhibits: ["ConversationView"]
leftPaneItem: viewCoordinator.getView("SettingsSidePanel")
leftPaneItem: viewCoordinator.getView("SettingsSidePanel", true)
Component.onCompleted: {
leftPaneItem.updateModel();
@ -76,7 +76,7 @@ ListSelectionView {
// Currently needed when changing the show link preview setting.
CurrentConversation.reloadInteractions();
if (UtilsAdapter.getAccountListSize() === 0) {
viewCoordinator.requestAppWindowWizardView();
viewCoordinator.present("WizardView");
} else {
AccountAdapter.changeAccount(0);
}
@ -85,8 +85,7 @@ ListSelectionView {
property int selectedMenu: index
rightPaneItem: StackView {
id: settingsView
objectName: "settingsView"
objectName: "SettingsLayout"
property var currentIndex: selectedMenu !== -1 ? selectedMenu : 0
anchors.fill: parent

View file

@ -237,6 +237,15 @@ SettingsPageBase {
}
}
ToggleSwitch {
id: useNativeWindowFrameCheckBox
Layout.fillWidth: true
checked: !UtilsAdapter.getAppValue(Settings.Key.UseFramelessWindow)
labelText: JamiStrings.useNativeWindowFrame
onSwitchToggled: UtilsAdapter.setAppValue(Settings.Key.UseFramelessWindow, !checked)
}
MaterialButton {
id: defaultSettings

View file

@ -29,7 +29,7 @@ RowLayout {
signal backArrowClicked
spacing: 10
BackButton {
BackButton { QWKSetParentHitTestVisible {}
id: backToSettingsMenuButton
Layout.preferredWidth: JamiTheme.preferredFieldHeight

View file

@ -25,11 +25,13 @@ import "../../commoncomponents"
JamiSplitView {
id: root
required property Item flickableContent
property real contentFlickableWidth: Math.min(JamiTheme.maximumWidthSettingsView, settingsPage.width - 2 * JamiTheme.preferredSettingsMarginSize)
property alias title: settingsPage.title
property color backgroundColor: JamiTheme.secondaryBackgroundColor
property alias pageContainer: settingsPage
Page {
id: settingsPage
SplitView.maximumWidth: root.width

View file

@ -108,7 +108,7 @@ SettingsPageBase {
}
ToggleSwitch {
id: applicationOnStartUpCheckBox
id: runOnStartUpCheckBox
Layout.fillWidth: true
checked: UtilsAdapter.checkStartupLink()

View file

@ -94,6 +94,8 @@ UtilsAdapter::setAppValue(const Settings::Key key, const QVariant& value)
Q_EMIT chatviewPositionChanged();
else if (key == Settings::Key::AppTheme)
Q_EMIT appThemeChanged();
else if (key == Settings::Key::UseFramelessWindow)
Q_EMIT useFramelessWindowChanged();
#ifndef APPSTORE
// Any donation campaign-related keys can trigger a donation campaign check
else if (key == Settings::Key::IsDonationVisible

View file

@ -172,6 +172,7 @@ Q_SIGNALS:
void showExperimentalCallSwarm();
void changeLanguage();
void donationCampaignSettingsChanged();
void useFramelessWindowChanged();
private:
QClipboard* clipboard_;

View file

@ -107,7 +107,7 @@ Item {
property real windowSize: windowPreferedSize > JamiTheme.minimumMapWidth ? windowPreferedSize : JamiTheme.minimumMapWidth
property real windowPreferedSize: root.maxWidth > root.maxHeight ? root.maxHeight / 3 : root.maxWidth / 3
property real xPos: 0
property real yPos: root.isUnpin ? 0 : JamiTheme.chatViewHeaderPreferredHeight
property real yPos: root.isUnpin ? 0 : JamiTheme.qwkTitleBarHeight
states: [
State {
@ -125,7 +125,7 @@ Item {
target: mapObject
parent: parentPin
x: xPos
y: JamiTheme.chatViewHeaderPreferredHeight
y: JamiTheme.qwkTitleBarHeight
}
}
]

View file

@ -55,10 +55,7 @@ BaseView {
function onCloseWizardView() {
root.dismiss();
viewCoordinator.preload("SidePanel");
viewCoordinator.preload("SettingsSidePanel");
viewCoordinator.present("WelcomePage");
viewCoordinator.preload("ConversationView");
}
}

View file

@ -402,7 +402,7 @@ Rectangle {
}
}
JamiPushButton {
JamiPushButton { QWKSetParentHitTestVisible {}
id: backButton
objectName: "createAccountPageBackButton"

View file

@ -382,7 +382,7 @@ Rectangle {
}
}
JamiPushButton {
JamiPushButton { QWKSetParentHitTestVisible {}
id: backButton
objectName: "welcomePageBackButton"

View file

@ -90,7 +90,7 @@ public Q_SLOTS:
// Create 2 Account
QSignalSpy accountStatusChangedSpy(&lrcInstance_->accountModel(),
&AccountModel::accountStatusChanged);
&AccountModel::accountStatusChanged);
QSignalSpy accountAddedSpy(&lrcInstance_->accountModel(), &AccountModel::accountAdded);
aliceId = lrcInstance_->accountModel().createNewAccount(profile::Type::JAMI, "Alice");

View file

@ -3,8 +3,22 @@
<file>src/tst_WizardView.qml</file>
<file>src/tst_NewSwarmPage.qml</file>
<file>src/tst_MessageOptions.qml</file>
<file>src/tst_OngoingCallPage.qml</file>
<file>src/tst_MainView.qml</file>
<file>src/tst_MaterialTextEdit.qml</file>
<file>src/tst_ChatView.qml</file>
<file>src/tst_ChatViewFooter.qml</file>
<file>src/tst_CachedImage.qml</file>
<file>src/tst_CallMessageDelegate.qml</file>
<file>src/tst_DataTransferMessageDelegate.qml</file>
<file>src/tst_FilesToSendContainer.qml</file>
<file>src/tst_PresenceIndicator.qml</file>
<file>src/tst_RecordBox.qml</file>
<file>src/tst_SettingsSidePanel.qml</file>
<file>src/tst_WelcomePage.qml</file>
<file>src/resources/gif_test.gif</file>
<file>src/resources/gz_test.gz</file>
<file>src/resources/png_test.png</file>
<file>src/TestWrapper.qml</file>
</qresource>
</RCC>

View file

@ -0,0 +1,39 @@
/*
* Copyright (C) 2024 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 "../../../src/app/"
// The purpose of this component is to fake the ApplicationWindow and prevent
// each UUT from having to manage its own top level app management objects
// (currently ViewManager, ViewCoordinator, and ApplicationWindow).
Item {
// This will be our UUT.
required default property var uut
// These are our fake app management objects. The caveat is that they
// must be maintained in sync with the actual objects in the app for now.
// The benefit, is that this should be the single place where we need to
// sync them.
property ViewManager viewManager: ViewManager {}
property ViewCoordinator viewCoordinator: ViewCoordinator {}
property ApplicationWindow appWindow: ApplicationWindow {
property bool useFrameless: false
}
}

View file

@ -39,9 +39,7 @@ ColumnLayout {
id: uut
property ViewManager viewManager: ViewManager {}
property ViewCoordinator viewCoordinator: ViewCoordinator {
viewManager: uut.viewManager
}
property ViewCoordinator viewCoordinator: ViewCoordinator {}
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: root.width

View file

@ -32,11 +32,8 @@ OngoingCallPage {
height: 600
property QtObject appWindow
property ViewManager viewManager: ViewManager {
}
property ViewCoordinator viewCoordinator: ViewCoordinator {
viewManager: uut.viewManager
}
property ViewManager viewManager: ViewManager {}
property ViewCoordinator viewCoordinator: ViewCoordinator {}
TestCase {
name: "Check basic visibility of action bar during a call"

View file

@ -16,40 +16,26 @@
*/
import QtQuick
import QtQuick.Controls
import QtTest
import "../../../src/app/"
import "../../../src/app/mainview/components"
WelcomePage {
id: uut
TestWrapper {
uut: WelcomePage {
TestCase {
name: "Open 'About Jami' popup"
width: 800
height: 600
function test_openAboutPopup() {
var aboutJamiButton = findChild(uut, "aboutJami")
aboutJamiButton.clicked()
// The appWindow, viewManager and viewCoordinator properties
// are required in order for the "aboutJami" button to work.
property Item appWindow: uut
property ViewManager viewManager: ViewManager {
}
property ViewCoordinator viewCoordinator: ViewCoordinator {
viewManager: uut.viewManager
}
TestCase {
name: "Open 'About Jami' popup"
function test_openAboutPopup() {
compare(viewManager.viewCount(), 0)
var aboutJamiButton = findChild(uut, "aboutJami")
aboutJamiButton.clicked()
compare(viewManager.viewCount(), 1)
var aboutJamiPopup = viewManager.getView("AboutPopUp")
verify(aboutJamiPopup !== null)
compare(aboutJamiPopup.visible, true)
var aboutJamiPopup = viewManager.getView("AboutPopUp")
verify(aboutJamiPopup !== null, "About Jami popup should be created")
compare(aboutJamiPopup.visible, true, "About Jami popup should be visible")
}
}
}
}
}