mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-08-04 14:55:43 +02:00
calloverlay: implement new action button design
- minor cosmetic changes to the call view overlay - add the ability to change audio/video input during a call from the call screen overlay Gitlab: #411 Change-Id: Id6bbb2278d807f3bd7ad1478db405669088584ce
This commit is contained in:
parent
3cca2e7078
commit
abde3cbac0
32 changed files with 1252 additions and 486 deletions
17
images/icons/add_people_black_24dp.svg
Normal file
17
images/icons/add_people_black_24dp.svg
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?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="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<path d="M16.1,11.3c1.1-0.7,1.8-2,1.8-3.3c0-2.2-1.8-4-4-4c-2.2,0-4,1.8-4,4c0,1.3,0.7,2.6,1.8,3.3c-0.6,0.3-1.2,0.6-1.8,1.1
|
||||
c-0.3-0.3-0.6-0.5-1-0.7c0.6-0.6,1-1.4,1-2.3c0-1.8-1.4-3.2-3.2-3.2c-1.8,0-3.2,1.4-3.2,3.2c0,0.9,0.4,1.7,1,2.3
|
||||
c-1.5,0.8-2.5,2.4-2.5,4.2c0,0.6,0.5,1.1,1.1,1.1h4.7c0,0.6,0.5,1.1,1.1,1.1h6.5l-0.3-0.2c-0.3-0.2-0.5-0.6-0.7-1l0-0.1H9.1
|
||||
c0-0.3,0.1-0.7,0.1-1c0.1-0.6,0.4-1.1,0.7-1.6c0.2-0.3,0.4-0.6,0.7-0.8c0.9-0.8,2.1-1.3,3.3-1.3c1.1,0,2.1,0.4,3,1l0.1,0.1l0.1-0.1
|
||||
c0.1-0.2,0.2-0.4,0.4-0.6c0.1-0.1,0.1-0.1,0.2-0.2l0.1-0.1l-0.1-0.1C17.2,11.8,16.6,11.5,16.1,11.3z M16.6,7.9
|
||||
c0,1.5-1.2,2.7-2.7,2.7c-1.5,0-2.7-1.2-2.7-2.7c0-1.5,1.2-2.7,2.7-2.7C15.4,5.2,16.6,6.4,16.6,7.9z M6.6,11.3c-1.1,0-2-0.9-2-2
|
||||
c0-1.1,0.9-2,2-2c1.1,0,2,0.9,2,2C8.6,10.4,7.7,11.3,6.6,11.3z M9,13.3c-0.5,0.7-0.9,1.5-1.1,2.4H3.2c0.1-1.8,1.6-3.3,3.4-3.3
|
||||
C7.5,12.4,8.4,12.7,9,13.3z"/>
|
||||
<path d="M21.2,15.6l-1.7,0l0-1.7c0-0.4-0.3-0.7-0.7-0.7c-0.2,0-0.4,0.1-0.5,0.2c-0.1,0.1-0.2,0.3-0.2,0.5l0,1.7l-1.7,0
|
||||
c-0.2,0-0.4,0.1-0.5,0.2c-0.1,0.1-0.2,0.3-0.2,0.5c0,0.4,0.3,0.7,0.7,0.7l1.7,0l0,1.7c0,0.4,0.3,0.7,0.7,0.7h0
|
||||
c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.5l0-1.7l1.7,0c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.5
|
||||
C21.9,16,21.6,15.7,21.2,15.6z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
17
images/icons/chat_black_24dp.svg
Normal file
17
images/icons/chat_black_24dp.svg
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?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="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<g id="Icones_Outline">
|
||||
<g id="Chat_Black_24dp">
|
||||
<g id="Shape" transform="translate(3.000000, 4.000000)">
|
||||
<g>
|
||||
<path d="M4.6,17c-0.2,0-0.4,0-0.6-0.1c-0.6-0.3-0.9-0.8-0.9-1.5V13H2.3C0.5,13-1,11.5-1,9.6V2.3C-1,0.5,0.5-1,2.3-1h13.3
|
||||
C17.5-1,19,0.5,19,2.3v7.3c0,1.8-1.5,3.3-3.3,3.3H9.5l-3.8,3.6C5.4,16.8,5,17,4.6,17z M2.3,0.4c-1.1,0-1.9,0.9-1.9,2v7.3
|
||||
c0,1,0.8,1.9,1.9,1.9h2.2v3.9c0,0,0,0.1,0.1,0.1s0.1,0,0.2,0l4.2-4h6.7c1,0,1.9-0.8,1.9-1.9V2.3c0-1-0.8-1.9-1.9-1.9H2.3
|
||||
L2.3,0.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 878 B |
7
images/icons/record_black_24dp.svg
Normal file
7
images/icons/record_black_24dp.svg
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?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="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<path d="M12,2C6.5,2,2,6.5,2,12s4.5,10,10,10s10-4.5,10-10S17.5,2,12,2z M12,20.3c-4.6,0-8.3-3.7-8.3-8.3S7.4,3.7,12,3.7
|
||||
s8.3,3.7,8.3,8.3S16.6,20.3,12,20.3z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 510 B |
21
images/icons/share_screen_black_24dp.svg
Normal file
21
images/icons/share_screen_black_24dp.svg
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?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="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<path id="Path" d="M12.6,8.9V7.8l2.6,2.2l-2.6,2.2V11c-3.4,0.1-4.2,2.6-4.2,2.6C8.5,9.6,11.7,9,12.6,8.9z"/>
|
||||
<g id="Icones_Outline">
|
||||
<g id="Laptop_Black_24dp">
|
||||
<g transform="translate(2.000000, 5.000000)">
|
||||
<g id="Shape">
|
||||
<path d="M17,12H2.8c-0.5,0-0.9-0.2-1.2-0.5C1.2,11.1,1,10.7,1,10.2V1.3c0-0.5,0.2-0.9,0.5-1.2s0.8-0.5,1.2-0.5H17
|
||||
c0.5,0,0.9,0.2,1.2,0.5c0.3,0.3,0.5,0.8,0.5,1.2v8.9c0,0.5-0.2,0.9-0.5,1.2S17.5,12,17,12z M2.8,0.9C2.7,0.9,2.6,1,2.5,1
|
||||
c0,0.1-0.1,0.2-0.1,0.3v8.9c0,0.1,0,0.2,0.1,0.3c0,0,0.1,0.1,0.3,0.1H17c0.1,0,0.2-0.1,0.3-0.1c0.1-0.1,0.1-0.2,0.1-0.3V1.3
|
||||
c0-0.1,0-0.2-0.1-0.3c0,0-0.1-0.1-0.3-0.1C17,0.9,2.8,0.9,2.8,0.9z"/>
|
||||
</g>
|
||||
<g id="Line-2">
|
||||
<path d="M19.5,14.4h-19c-0.4,0-0.7-0.3-0.7-0.7S0.1,13,0.5,13h19c0.4,0,0.7,0.3,0.7,0.7S19.9,14.4,19.5,14.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
14
images/icons/spk_black_24dp.svg
Normal file
14
images/icons/spk_black_24dp.svg
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?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="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M14.6,4c-0.4-0.7-1.3-0.8-2-0.4L6.8,7.7H4.1C3,7.7,2,8.7,2,9.9v4.3c0,1.2,1,2.1,2.1,2.1h2.6l5.8,4.1
|
||||
c0.2,0.2,0.5,0.2,0.8,0.2c0.8,0,1.4-0.6,1.5-1.4V4.8C14.9,4.5,14.8,4.2,14.6,4z M13.4,19.2L13.4,19.2l-5.7-4l-0.4-0.3h-1H6.1h-2
|
||||
c-0.4,0-0.7-0.3-0.7-0.7V9.9c0-0.4,0.3-0.7,0.7-0.7H6h0.2h1l0.4-0.3l5.7-4L13.4,19.2L13.4,19.2z"/>
|
||||
<path d="M19.1,12c0,1.5-0.6,2.9-1.6,4c-0.3,0.3-0.7,0.3-1,0c-0.3-0.3-0.3-0.7,0-1c1.6-1.7,1.6-4.3,0-6c-0.3-0.3-0.3-0.7,0-1
|
||||
c0.3-0.3,0.7-0.3,1,0C18.6,9.1,19.1,10.5,19.1,12z"/>
|
||||
<path d="M18.9,5.1c-0.3-0.3-0.7-0.2-1,0c-0.3,0.3-0.2,0.7,0,1c3.3,2.9,3.5,8,0.6,11.2c-0.2,0.2-0.4,0.4-0.6,0.6
|
||||
c-0.3,0.3-0.3,0.7,0,1c0.3,0.3,0.7,0.3,1,0c3.8-3.5,4.1-9.4,0.7-13.2C19.4,5.5,19.1,5.3,18.9,5.1L18.9,5.1z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
18
images/icons/spk_none_black_24dp.svg
Normal file
18
images/icons/spk_none_black_24dp.svg
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?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="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M14.6,4c-0.4-0.7-1.3-0.8-2-0.4L6.8,7.7H4.1C3,7.7,2,8.7,2,9.9v4.3c0,1.2,1,2.1,2.1,2.1h2.6l5.8,4.1
|
||||
c0.2,0.2,0.5,0.2,0.8,0.2c0.8,0,1.4-0.6,1.5-1.4V4.8C14.9,4.5,14.8,4.2,14.6,4z M13.4,19.2L13.4,19.2l-5.7-4l-0.4-0.3h-1H6.1h-2
|
||||
c-0.4,0-0.7-0.3-0.7-0.7V9.9c0-0.4,0.3-0.7,0.7-0.7H6h0.2h1l0.4-0.3l5.7-4L13.4,19.2L13.4,19.2z"/>
|
||||
<path d="M19.1,12c0,1.5-0.6,2.9-1.6,4c-0.3,0.3-0.7,0.3-1,0c-0.3-0.3-0.3-0.7,0-1c1.6-1.7,1.6-4.3,0-6c-0.3-0.3-0.3-0.7,0-1
|
||||
c0.3-0.3,0.7-0.3,1,0C18.6,9.1,19.1,10.5,19.1,12z"/>
|
||||
<path d="M18.9,5.1c-0.3-0.3-0.7-0.2-1,0c-0.3,0.3-0.2,0.7,0,1c3.3,2.9,3.5,8,0.6,11.2c-0.2,0.2-0.4,0.4-0.6,0.6
|
||||
c-0.3,0.3-0.3,0.7,0,1c0.3,0.3,0.7,0.3,1,0c3.8-3.5,4.1-9.4,0.7-13.2C19.4,5.5,19.1,5.3,18.9,5.1L18.9,5.1z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M3.3,20.7c-0.2,0-0.3-0.1-0.5-0.2c-0.3-0.3-0.3-0.7-0.1-1L16.3,3.7c0.3-0.3,0.7-0.3,1-0.1c0.3,0.3,0.3,0.7,0.1,1L3.8,20.5
|
||||
C3.7,20.6,3.5,20.7,3.3,20.7z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
6
qml.qrc
6
qml.qrc
|
@ -106,7 +106,6 @@
|
|||
<file>src/mainview/components/CallStackView.qml</file>
|
||||
<file>src/mainview/components/InitialCallPage.qml</file>
|
||||
<file>src/mainview/components/CallOverlay.qml</file>
|
||||
<file>src/mainview/components/CallOverlayButtonGroup.qml</file>
|
||||
<file>src/mainview/components/ContactSearchBar.qml</file>
|
||||
<file>src/mainview/components/OngoingCallPage.qml</file>
|
||||
<file>src/mainview/components/ParticipantOverlay.qml</file>
|
||||
|
@ -137,5 +136,10 @@
|
|||
<file>src/mainview/components/SmartListItemDelegate.qml</file>
|
||||
<file>src/mainview/components/BadgeNotifier.qml</file>
|
||||
<file>src/mainview/components/ParticipantsLayer.qml</file>
|
||||
<file>src/mainview/components/MainOverlay.qml</file>
|
||||
<file>src/mainview/components/CallButtonDelegate.qml</file>
|
||||
<file>src/mainview/components/CallActionBar.qml</file>
|
||||
<file>src/commoncomponents/HalfPill.qml</file>
|
||||
<file>src/commoncomponents/MaterialToolTip.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
@ -134,5 +134,11 @@
|
|||
<file>images/icons/settings-24px.svg</file>
|
||||
<file>images/icons/quote.svg</file>
|
||||
<file>images/icons/plugins-24px.svg</file>
|
||||
<file>images/icons/record_black_24dp.svg</file>
|
||||
<file>images/icons/share_screen_black_24dp.svg</file>
|
||||
<file>images/icons/chat_black_24dp.svg</file>
|
||||
<file>images/icons/add_people_black_24dp.svg</file>
|
||||
<file>images/icons/spk_black_24dp.svg</file>
|
||||
<file>images/icons/spk_none_black_24dp.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
@ -62,6 +62,8 @@ AudioDeviceModel::data(const QModelIndex& index, int role) const
|
|||
}
|
||||
case Role::RawDeviceName:
|
||||
return QVariant(devices_.at(index.row()));
|
||||
case Role::isCurrent:
|
||||
return QVariant(index.row() == getCurrentIndex());
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -115,7 +117,7 @@ AudioDeviceModel::reset()
|
|||
}
|
||||
|
||||
int
|
||||
AudioDeviceModel::getCurrentIndex()
|
||||
AudioDeviceModel::getCurrentIndex() const
|
||||
{
|
||||
QString currentId = lrcInstance_->avModel().getInputDevice();
|
||||
auto resultList = match(index(0, 0), Qt::DisplayRole, QVariant(currentId));
|
||||
|
|
|
@ -28,7 +28,7 @@ public:
|
|||
Q_ENUM(Type)
|
||||
Q_PROPERTY(Type type MEMBER type_ NOTIFY typeChanged)
|
||||
|
||||
enum Role { DeviceName = Qt::UserRole + 1, RawDeviceName };
|
||||
enum Role { DeviceName = Qt::UserRole + 1, RawDeviceName, isCurrent };
|
||||
Q_ENUM(Role)
|
||||
|
||||
Q_SIGNALS:
|
||||
|
@ -56,7 +56,7 @@ public:
|
|||
Qt::ItemFlags flags(const QModelIndex& index) const override;
|
||||
|
||||
Q_INVOKABLE void reset();
|
||||
Q_INVOKABLE int getCurrentIndex();
|
||||
Q_INVOKABLE int getCurrentIndex() const;
|
||||
|
||||
private:
|
||||
QVector<QString> devices_;
|
||||
|
|
|
@ -69,13 +69,19 @@ AvAdapter::populateVideoDeviceContextMenuItem()
|
|||
}
|
||||
|
||||
void
|
||||
AvAdapter::onVideoContextMenuDeviceItemClicked(const QString& deviceName)
|
||||
AvAdapter::selectVideoInputDeviceByName(const QString& deviceName)
|
||||
{
|
||||
auto deviceId = lrcInstance_->avModel().getDeviceIdFromName(deviceName);
|
||||
if (deviceId.isEmpty()) {
|
||||
qWarning() << "Couldn't find device: " << deviceName;
|
||||
return;
|
||||
}
|
||||
selectVideoInputDeviceById(deviceId);
|
||||
}
|
||||
|
||||
void
|
||||
AvAdapter::selectVideoInputDeviceById(const QString& deviceId)
|
||||
{
|
||||
lrcInstance_->avModel().setCurrentVideoCaptureDevice(deviceId);
|
||||
lrcInstance_->avModel().switchInputTo(deviceId, getCurrentCallId());
|
||||
}
|
||||
|
|
|
@ -45,8 +45,11 @@ protected:
|
|||
// Return needed info for populating video device context menu item.
|
||||
Q_INVOKABLE QVariantMap populateVideoDeviceContextMenuItem();
|
||||
|
||||
// Preview video input switching.
|
||||
Q_INVOKABLE void onVideoContextMenuDeviceItemClicked(const QString& deviceName);
|
||||
// switch preview video input by device name
|
||||
Q_INVOKABLE void selectVideoInputDeviceByName(const QString& deviceName);
|
||||
|
||||
// switch preview video input by device id
|
||||
Q_INVOKABLE void selectVideoInputDeviceById(const QString& deviceId);
|
||||
|
||||
// Share the screen specificed by screen number.
|
||||
Q_INVOKABLE void shareEntireScreen(int screenNumber);
|
||||
|
|
|
@ -639,7 +639,6 @@ CallAdapter::updateCallOverlay(const lrc::api::conversation::Info& convInfo)
|
|||
isVideoMuted,
|
||||
isRecording,
|
||||
accInfo.profileInfo.type == lrc::api::profile::Type::SIP,
|
||||
!convInfo.confId.isEmpty(),
|
||||
bestName);
|
||||
}
|
||||
|
||||
|
|
|
@ -95,7 +95,6 @@ Q_SIGNALS:
|
|||
bool isVideoMuted,
|
||||
bool isRecording,
|
||||
bool isSIP,
|
||||
bool isConferenceCall,
|
||||
const QString& bestName);
|
||||
void remoteRecordingChanged(const QStringList& peers, bool state);
|
||||
void eraseRemoteRecording();
|
||||
|
|
|
@ -44,15 +44,10 @@ CallControlListModel::data(const QModelIndex& index, int role) const
|
|||
auto item = data_.at(index.row());
|
||||
|
||||
switch (role) {
|
||||
case Role::DummyRole:
|
||||
break;
|
||||
#define X(t, role) \
|
||||
case Role::role: \
|
||||
return QVariant::fromValue(item.role);
|
||||
CC_ROLES
|
||||
#undef X
|
||||
default:
|
||||
break;
|
||||
case Role::ItemAction:
|
||||
return QVariant::fromValue(item.itemAction);
|
||||
case Role::BadgeCount:
|
||||
return QVariant::fromValue(item.badgeCount);
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
@ -62,9 +57,8 @@ CallControlListModel::roleNames() const
|
|||
{
|
||||
using namespace CallControl;
|
||||
QHash<int, QByteArray> roles;
|
||||
#define X(t, role) roles[role] = #role;
|
||||
CC_ROLES
|
||||
#undef X
|
||||
roles[ItemAction] = "ItemAction";
|
||||
roles[BadgeCount] = "BadgeCount";
|
||||
return roles;
|
||||
}
|
||||
|
||||
|
@ -73,7 +67,7 @@ CallControlListModel::setBadgeCount(int row, int count)
|
|||
{
|
||||
if (row >= rowCount())
|
||||
return;
|
||||
data_[row].BadgeCount = count;
|
||||
data_[row].badgeCount = count;
|
||||
auto idx = index(row, 0);
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
}
|
||||
|
@ -86,6 +80,12 @@ CallControlListModel::addItem(const CallControl::Item& item)
|
|||
endResetModel();
|
||||
}
|
||||
|
||||
void
|
||||
CallControlListModel::clearData()
|
||||
{
|
||||
data_.clear();
|
||||
}
|
||||
|
||||
IndexRangeFilterProxyModel::IndexRangeFilterProxyModel(QAbstractListModel* parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
|
@ -129,25 +129,15 @@ CallOverlayModel::CallOverlayModel(LRCInstance* instance, QObject* parent)
|
|||
}
|
||||
|
||||
void
|
||||
CallOverlayModel::addPrimaryControl(const QVariantMap& props)
|
||||
CallOverlayModel::addPrimaryControl(const QVariant& action)
|
||||
{
|
||||
CallControl::Item item {
|
||||
#define X(t, role) props[#role].value<t>(),
|
||||
CC_ROLES
|
||||
#undef X
|
||||
};
|
||||
primaryModel_->addItem(item);
|
||||
primaryModel_->addItem(CallControl::Item {action.value<QObject*>()});
|
||||
}
|
||||
|
||||
void
|
||||
CallOverlayModel::addSecondaryControl(const QVariantMap& props)
|
||||
CallOverlayModel::addSecondaryControl(const QVariant& action)
|
||||
{
|
||||
CallControl::Item item {
|
||||
#define X(t, role) props[#role].value<t>(),
|
||||
CC_ROLES
|
||||
#undef X
|
||||
};
|
||||
secondaryModel_->addItem(item);
|
||||
secondaryModel_->addItem(CallControl::Item {action.value<QObject*>()});
|
||||
setControlRanges();
|
||||
}
|
||||
|
||||
|
@ -187,6 +177,13 @@ CallOverlayModel::overflowHiddenModel()
|
|||
return QVariant::fromValue(overflowHiddenModel_);
|
||||
}
|
||||
|
||||
void
|
||||
CallOverlayModel::clearControls()
|
||||
{
|
||||
primaryModel_->clearData();
|
||||
secondaryModel_->clearData();
|
||||
}
|
||||
|
||||
void
|
||||
CallOverlayModel::registerFilter(QQuickWindow* object, QQuickItem* item)
|
||||
{
|
||||
|
|
|
@ -27,28 +27,15 @@
|
|||
#include <QSortFilterProxyModel>
|
||||
#include <QQuickItem>
|
||||
|
||||
#define CC_ROLES \
|
||||
X(QObject*, ItemAction) \
|
||||
X(int, BadgeCount) \
|
||||
X(bool, HasBackground) \
|
||||
X(QObject*, MenuAction) \
|
||||
X(QString, Name)
|
||||
|
||||
namespace CallControl {
|
||||
Q_NAMESPACE
|
||||
enum Role {
|
||||
DummyRole = Qt::UserRole + 1,
|
||||
#define X(t, role) role,
|
||||
CC_ROLES
|
||||
#undef X
|
||||
};
|
||||
enum Role { ItemAction = Qt::UserRole + 1, BadgeCount };
|
||||
Q_ENUM_NS(Role)
|
||||
|
||||
struct Item
|
||||
{
|
||||
#define X(t, role) t role;
|
||||
CC_ROLES
|
||||
#undef X
|
||||
QObject* itemAction;
|
||||
int badgeCount {0};
|
||||
};
|
||||
} // namespace CallControl
|
||||
|
||||
|
@ -64,6 +51,7 @@ public:
|
|||
|
||||
void setBadgeCount(int row, int count);
|
||||
void addItem(const CallControl::Item& item);
|
||||
void clearData();
|
||||
|
||||
private:
|
||||
QList<CallControl::Item> data_;
|
||||
|
@ -93,9 +81,10 @@ class CallOverlayModel : public QObject
|
|||
public:
|
||||
CallOverlayModel(LRCInstance* instance, QObject* parent = nullptr);
|
||||
|
||||
Q_INVOKABLE void addPrimaryControl(const QVariantMap& props);
|
||||
Q_INVOKABLE void addSecondaryControl(const QVariantMap& props);
|
||||
Q_INVOKABLE void addPrimaryControl(const QVariant& action);
|
||||
Q_INVOKABLE void addSecondaryControl(const QVariant& action);
|
||||
Q_INVOKABLE void setBadgeCount(int row, int count);
|
||||
Q_INVOKABLE void clearControls();
|
||||
|
||||
Q_INVOKABLE QVariant primaryModel();
|
||||
Q_INVOKABLE QVariant secondaryModel();
|
||||
|
|
55
src/commoncomponents/HalfPill.qml
Normal file
55
src/commoncomponents/HalfPill.qml
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (C) 2021 by Savoir-faire Linux
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick 2.14
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
enum Type {
|
||||
None,
|
||||
Top,
|
||||
Left,
|
||||
Bottom,
|
||||
Right
|
||||
}
|
||||
|
||||
property int type: HalfPill.None
|
||||
property int radius: 0
|
||||
property alias color: rect.color
|
||||
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
id: rect
|
||||
|
||||
property bool horizontal: type === HalfPill.Left ||
|
||||
type == HalfPill.Right
|
||||
property bool direction: type === HalfPill.Right ||
|
||||
type == HalfPill.Bottom
|
||||
|
||||
radius: root.radius * (type !== HalfPill.None)
|
||||
width: root.size + radius
|
||||
height: root.size + radius
|
||||
anchors.fill: root
|
||||
anchors.leftMargin: horizontal * direction * -radius
|
||||
anchors.rightMargin: horizontal * !direction * -radius
|
||||
anchors.topMargin: !horizontal * direction * -radius
|
||||
anchors.bottomMargin: !horizontal * !direction * -radius
|
||||
}
|
||||
}
|
57
src/commoncomponents/MaterialToolTip.qml
Normal file
57
src/commoncomponents/MaterialToolTip.qml
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (C) 2021 by Savoir-faire Linux
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.14
|
||||
|
||||
import net.jami.Constants 1.0
|
||||
|
||||
ToolTip {
|
||||
id: root
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible)
|
||||
animation.start()
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
id: label
|
||||
text: root.text
|
||||
font: root.font
|
||||
color: "white"
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "#c4272727"
|
||||
radius: 5
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: animation
|
||||
NumberAnimation {
|
||||
target: root; properties: "opacity"
|
||||
from: 0; to: 1.0
|
||||
duration: JamiTheme.shortFadeDuration
|
||||
}
|
||||
NumberAnimation {
|
||||
target: root; properties: "scale"
|
||||
from: 0.5; to: 1.0
|
||||
duration: JamiTheme.shortFadeDuration * 0.5
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,8 +25,8 @@ import net.jami.Models 1.0
|
|||
Image {
|
||||
id: root
|
||||
|
||||
property real containerWidth
|
||||
property real containerHeight
|
||||
property real containerWidth: 30
|
||||
property real containerHeight: 30
|
||||
|
||||
property int padding: 0
|
||||
property point offset: Qt.point(0, 0)
|
||||
|
|
|
@ -186,7 +186,7 @@ Item {
|
|||
property string peerStoppedRecording: qsTr("Peer stopped recording")
|
||||
property string isCallingYou: qsTr("is calling you")
|
||||
|
||||
// CallOverlayButtonGroup
|
||||
// CallOverlay
|
||||
property string mute: qsTr("Mute")
|
||||
property string unmute: qsTr("Unmute")
|
||||
property string hangup: qsTr("End call")
|
||||
|
|
|
@ -164,8 +164,9 @@ Item {
|
|||
property color bgDarkMode_: rgba256(32, 32, 32, 100)
|
||||
|
||||
property int shortFadeDuration: 150
|
||||
property int overlayFadeDelay: 2000
|
||||
property int overlayFadeDuration: 500
|
||||
property int recordBlinkDuration: 500
|
||||
property int overlayFadeDelay: 4000
|
||||
property int overlayFadeDuration: 250
|
||||
property int smartListTransitionDuration: 120
|
||||
|
||||
// Sizes
|
||||
|
|
382
src/mainview/components/CallActionBar.qml
Normal file
382
src/mainview/components/CallActionBar.qml
Normal file
|
@ -0,0 +1,382 @@
|
|||
/*
|
||||
* Copyright (C) 2021 by Savoir-faire Linux
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
|
||||
import net.jami.Models 1.0
|
||||
import net.jami.Adapters 1.0
|
||||
import net.jami.Constants 1.0
|
||||
|
||||
import "../../commoncomponents"
|
||||
|
||||
Control {
|
||||
id: root
|
||||
|
||||
property alias overflowOpen: overflowButton.popup.visible
|
||||
property bool subMenuOpen: false
|
||||
|
||||
property real itemSpacing: 2
|
||||
property bool localIsRecording: false
|
||||
|
||||
signal chatClicked
|
||||
signal addToConferenceClicked
|
||||
signal transferClicked // TODO: bind this
|
||||
signal shareScreenClicked
|
||||
signal shareScreenAreaClicked // TODO: bind this
|
||||
signal pluginsClicked
|
||||
|
||||
Component {
|
||||
id: buttonDelegate
|
||||
|
||||
CallButtonDelegate {
|
||||
width: root.height
|
||||
height: width
|
||||
onSubMenuVisibleChanged: subMenuOpen = subMenuVisible
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AvAdapter
|
||||
|
||||
// TODO: audio device list updates
|
||||
function onVideoDeviceListChanged(listIsEmpty) {
|
||||
videoInputDeviceListModel.reset();
|
||||
}
|
||||
}
|
||||
|
||||
property list<Action> menuActions: [
|
||||
Action {
|
||||
id: audioInputMenuAction
|
||||
text: JamiStrings.selectAudioInputDevice
|
||||
property var listModel: AudioDeviceModel {
|
||||
id: audioInputDeviceListModel
|
||||
lrcInstance: LRCInstance
|
||||
type: AudioDeviceModel.Type.Record
|
||||
}
|
||||
function accept(index) {
|
||||
AvAdapter.stopAudioMeter(false)
|
||||
AVModel.setInputDevice(listModel.data(
|
||||
listModel.index(index, 0),
|
||||
AudioDeviceModel.RawDeviceName))
|
||||
AvAdapter.startAudioMeter(false)
|
||||
}
|
||||
},
|
||||
Action {
|
||||
id: audioOutputMenuAction
|
||||
text: JamiStrings.selectAudioOutputDevice
|
||||
property var listModel: AudioDeviceModel {
|
||||
id: audioOutputDeviceListModel
|
||||
lrcInstance: LRCInstance
|
||||
type: AudioDeviceModel.Type.Playback
|
||||
}
|
||||
function accept(index) {
|
||||
AvAdapter.stopAudioMeter(false)
|
||||
AVModel.setOutputDevice(listModel.data(
|
||||
listModel.index(index, 0),
|
||||
AudioDeviceModel.RawDeviceName))
|
||||
AvAdapter.startAudioMeter(false)
|
||||
}
|
||||
},
|
||||
Action {
|
||||
id: videoInputMenuAction
|
||||
text: JamiStrings.selectVideoDevice
|
||||
property var listModel: VideoInputDeviceModel {
|
||||
id: videoInputDeviceListModel
|
||||
lrcInstance: LRCInstance
|
||||
}
|
||||
function accept(index) {
|
||||
if(listModel.deviceCount() < 1)
|
||||
return
|
||||
try {
|
||||
var deviceId = listModel.data(
|
||||
listModel.index(index, 0),
|
||||
VideoInputDeviceModel.DeviceId)
|
||||
var deviceName = listModel.data(
|
||||
listModel.index(index, 0),
|
||||
VideoInputDeviceModel.DeviceName)
|
||||
if(deviceId.length === 0) {
|
||||
console.warn("Couldn't find device: " + deviceName)
|
||||
return
|
||||
}
|
||||
if (AVModel.getCurrentVideoCaptureDevice() !== deviceId) {
|
||||
AVModel.setCurrentVideoCaptureDevice(deviceId)
|
||||
AVModel.setDefaultDevice(deviceId)
|
||||
}
|
||||
AvAdapter.selectVideoInputDeviceById(deviceId)
|
||||
} catch(err){ console.warn(err.message) }
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
property list<Action> primaryActions: [
|
||||
Action {
|
||||
id: muteAudioAction
|
||||
onTriggered: CallAdapter.muteThisCallToggle()
|
||||
checkable: true
|
||||
icon.source: checked ?
|
||||
"qrc:/images/icons/mic_off-24px.svg" :
|
||||
"qrc:/images/icons/mic-24px.svg"
|
||||
icon.color: checked ? "red" : "white"
|
||||
text: !checked ? JamiStrings.mute : JamiStrings.unmute
|
||||
property var menuAction: audioInputMenuAction
|
||||
},
|
||||
Action {
|
||||
id: hangupAction
|
||||
onTriggered: CallAdapter.hangUpThisCall()
|
||||
icon.source: "qrc:/images/icons/ic_call_end_white_24px.svg"
|
||||
icon.color: "white"
|
||||
text: JamiStrings.hangup
|
||||
property bool hasBg: true
|
||||
},
|
||||
Action {
|
||||
id: muteVideoAction
|
||||
onTriggered: CallAdapter.videoPauseThisCallToggle()
|
||||
checkable: true
|
||||
icon.source: checked ?
|
||||
"qrc:/images/icons/videocam_off-24px.svg" :
|
||||
"qrc:/images/icons/videocam-24px.svg"
|
||||
icon.color: checked ? "red" : "white"
|
||||
text: !checked ? JamiStrings.pauseVideo : JamiStrings.resumeVideo
|
||||
property var menuAction: videoInputMenuAction
|
||||
}
|
||||
]
|
||||
|
||||
property list<Action> secondaryActions: [
|
||||
Action {
|
||||
id: audioOutputAction
|
||||
// temp hack for missing back-end, just open device selection
|
||||
property bool bypassMuteAction: true
|
||||
checkable: !bypassMuteAction
|
||||
icon.source: "qrc:/images/icons/spk_black_24dp.svg"
|
||||
icon.color: "white"
|
||||
text: JamiStrings.selectAudioOutputDevice
|
||||
property var menuAction: audioOutputMenuAction
|
||||
},
|
||||
Action {
|
||||
id: addPersonAction
|
||||
onTriggered: root.addToConferenceClicked()
|
||||
icon.source: "qrc:/images/icons/add_people_black_24dp.svg"
|
||||
icon.color: "white"
|
||||
text: JamiStrings.addParticipants
|
||||
},
|
||||
Action {
|
||||
id: chatAction
|
||||
onTriggered: root.chatClicked()
|
||||
icon.source: "qrc:/images/icons/chat_black_24dp.svg"
|
||||
icon.color: "white"
|
||||
text: JamiStrings.chat
|
||||
},
|
||||
Action {
|
||||
id: shareAction
|
||||
onTriggered: root.shareScreenClicked()
|
||||
icon.source: "qrc:/images/icons/share_screen_black_24dp.svg"
|
||||
icon.color: "white"
|
||||
text: JamiStrings.shareScreen
|
||||
property real size: 34
|
||||
},
|
||||
Action {
|
||||
id: recordAction
|
||||
onTriggered: CallAdapter.recordThisCallToggle()
|
||||
checkable: true
|
||||
icon.source: "qrc:/images/icons/record_black_24dp.svg"
|
||||
icon.color: checked ? "red" : "white"
|
||||
text: !checked ? JamiStrings.startRec : JamiStrings.stopRec
|
||||
property bool blinksWhenChecked: true
|
||||
property real size: 28
|
||||
},
|
||||
Action {
|
||||
id: pluginsAction
|
||||
onTriggered: root.pluginsClicked()
|
||||
icon.source: "qrc:/images/icons/plugins-24px.svg"
|
||||
icon.color: "white"
|
||||
text: JamiStrings.viewPlugin
|
||||
enabled: UtilsAdapter.checkShowPluginsButton(true)
|
||||
}
|
||||
]
|
||||
|
||||
property var overflowItemCount
|
||||
|
||||
Connections {
|
||||
target: callOverlay
|
||||
function onIsAudioOnlyChanged() { reset() }
|
||||
function onIsSIPChanged() { reset() }
|
||||
function onIsModeratorChanged() { reset() }
|
||||
}
|
||||
|
||||
function reset() {
|
||||
CallOverlayModel.clearControls();
|
||||
|
||||
// centered controls
|
||||
CallOverlayModel.addPrimaryControl(muteAudioAction)
|
||||
CallOverlayModel.addPrimaryControl(hangupAction)
|
||||
if (!isAudioOnly)
|
||||
CallOverlayModel.addPrimaryControl(muteVideoAction)
|
||||
|
||||
// overflow controls
|
||||
CallOverlayModel.addSecondaryControl(audioOutputAction)
|
||||
if (isModerator && !isSIP)
|
||||
CallOverlayModel.addSecondaryControl(addPersonAction)
|
||||
CallOverlayModel.addSecondaryControl(chatAction)
|
||||
if (!isAudioOnly)
|
||||
CallOverlayModel.addSecondaryControl(shareAction)
|
||||
CallOverlayModel.addSecondaryControl(recordAction)
|
||||
if (UtilsAdapter.checkShowPluginsButton(true))
|
||||
CallOverlayModel.addSecondaryControl(pluginsAction)
|
||||
overflowItemCount = CallOverlayModel.secondaryModel().rowCount()
|
||||
|
||||
muteAudioAction.checked = isAudioMuted
|
||||
muteVideoAction.checked = isVideoMuted
|
||||
}
|
||||
|
||||
Item {
|
||||
id: centralControls
|
||||
anchors.centerIn: parent
|
||||
width: childrenRect.width
|
||||
height: root.height
|
||||
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
|
||||
ListView {
|
||||
property bool centeredGroup: true
|
||||
|
||||
orientation: ListView.Horizontal
|
||||
implicitWidth: contentWidth
|
||||
implicitHeight: contentHeight
|
||||
|
||||
model: CallOverlayModel.primaryModel()
|
||||
delegate: buttonDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
id: overflowRect
|
||||
property real remainingSpace: (root.width - centralControls.width) / 2
|
||||
anchors.right: parent.right
|
||||
width: childrenRect.width
|
||||
height: root.height
|
||||
|
||||
RowLayout {
|
||||
spacing: itemSpacing
|
||||
|
||||
ListView {
|
||||
id: overflowItemListView
|
||||
|
||||
orientation: ListView.Horizontal
|
||||
implicitWidth: contentWidth
|
||||
implicitHeight: overflowRect.height
|
||||
|
||||
spacing: itemSpacing
|
||||
|
||||
property int overflowIndex: {
|
||||
var maxItems = Math.floor((overflowRect.remainingSpace - 24) / root.height) - 1
|
||||
return Math.min(overflowItemCount, maxItems)
|
||||
}
|
||||
property int nOverflowItems: overflowItemCount - overflowIndex
|
||||
onNOverflowItemsChanged: {
|
||||
var diff = overflowItemListView.count - nOverflowItems
|
||||
var effectiveOverflowIndex = overflowIndex
|
||||
if (effectiveOverflowIndex === overflowItemCount - 1)
|
||||
effectiveOverflowIndex += diff
|
||||
|
||||
CallOverlayModel.overflowIndex = effectiveOverflowIndex
|
||||
}
|
||||
|
||||
model: CallOverlayModel.overflowModel()
|
||||
delegate: buttonDelegate
|
||||
}
|
||||
ComboBox {
|
||||
id: overflowButton
|
||||
|
||||
visible: CallOverlayModel.overflowIndex < overflowItemCount
|
||||
width: root.height
|
||||
height: width
|
||||
|
||||
model: CallOverlayModel.overflowHiddenModel()
|
||||
|
||||
delegate: buttonDelegate
|
||||
|
||||
indicator: null
|
||||
|
||||
contentItem: Text {
|
||||
text: "⋮"
|
||||
color: "white"
|
||||
font.pointSize: 24
|
||||
font.weight: Font.Bold
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: root.height
|
||||
implicitHeight: implicitWidth
|
||||
color: overflowButton.down ?
|
||||
"#80aaaaaa" :
|
||||
overflowButton.hovered ?
|
||||
"#80777777" :
|
||||
"#80444444"
|
||||
}
|
||||
|
||||
Item {
|
||||
implicitHeight: children[0].contentHeight
|
||||
width: overflowButton.width
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: itemSpacing
|
||||
visible: !overflowButton.popup.visible
|
||||
ListView {
|
||||
spacing: itemSpacing
|
||||
anchors.fill: parent
|
||||
model: CallOverlayModel.overflowVisibleModel()
|
||||
delegate: buttonDelegate
|
||||
ScrollIndicator.vertical: ScrollIndicator {}
|
||||
|
||||
add: Transition {
|
||||
NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 80 }
|
||||
NumberAnimation { property: "scale"; from: 0; to: 1.0; duration: 80 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
popup: Popup {
|
||||
y: overflowButton.height + itemSpacing
|
||||
width: overflowButton.width
|
||||
implicitHeight: contentItem.implicitHeight
|
||||
padding: 0
|
||||
|
||||
contentItem: ListView {
|
||||
id: overflowListView
|
||||
spacing: itemSpacing
|
||||
implicitHeight: contentHeight
|
||||
model: overflowButton.popup.visible ?
|
||||
overflowButton.delegateModel :
|
||||
null
|
||||
|
||||
ScrollIndicator.vertical: ScrollIndicator {}
|
||||
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
304
src/mainview/components/CallButtonDelegate.qml
Normal file
304
src/mainview/components/CallButtonDelegate.qml
Normal file
|
@ -0,0 +1,304 @@
|
|||
/*
|
||||
* Copyright (C) 2021 by Savoir-faire Linux
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtGraphicalEffects 1.14
|
||||
|
||||
import net.jami.Models 1.0
|
||||
import net.jami.Constants 1.0
|
||||
|
||||
import "../../commoncomponents"
|
||||
|
||||
ItemDelegate {
|
||||
id: wrapper
|
||||
|
||||
property bool isFirst: index < 1
|
||||
property bool isLast: index + 1 < ListView.view.count ? false : true
|
||||
property bool hasLast: ListView.view.centeredGroup !== undefined
|
||||
property bool isVertical: wrapper.ListView.view.orientation === ListView.Vertical
|
||||
|
||||
property alias subMenuVisible: menu.popup.visible
|
||||
|
||||
action: ItemAction
|
||||
checkable: ItemAction.checkable
|
||||
|
||||
// hide the action's visual elements like the blurry looking icon
|
||||
icon.source: ""
|
||||
text: ""
|
||||
|
||||
z: index
|
||||
|
||||
// TODO: remove this when output volume control is implemented
|
||||
MouseArea {
|
||||
visible: ItemAction.bypassMuteAction !== undefined &&
|
||||
ItemAction.bypassMuteAction && !menu.popup.visible
|
||||
anchors.fill: wrapper
|
||||
onClicked: menu.popup.open()
|
||||
}
|
||||
|
||||
background: HalfPill {
|
||||
anchors.fill: parent
|
||||
radius: 5
|
||||
color: {
|
||||
if (supplimentaryBackground.visible)
|
||||
return "#c4272727"
|
||||
return wrapper.down ?
|
||||
"#c4777777" :
|
||||
wrapper.hovered && !menu.hovered ?
|
||||
"#c4444444" :
|
||||
"#c4272727"
|
||||
}
|
||||
type: {
|
||||
if (isVertical) {
|
||||
if (isFirst)
|
||||
return HalfPill.Top
|
||||
else if (isLast && hasLast)
|
||||
return HalfPill.Bottom
|
||||
} else {
|
||||
if (isFirst)
|
||||
return HalfPill.Left
|
||||
else if (isLast && hasLast)
|
||||
return HalfPill.Right
|
||||
}
|
||||
return HalfPill.None
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: JamiTheme.shortFadeDuration }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: supplimentaryBackground
|
||||
|
||||
visible: ItemAction.hasBg !== undefined
|
||||
color: wrapper.down ?
|
||||
Qt.lighter(JamiTheme.refuseRed, 1.5) :
|
||||
wrapper.hovered && !menu.hovered ?
|
||||
JamiTheme.refuseRed :
|
||||
JamiTheme.refuseRedTransparent
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: JamiTheme.shortFadeDuration }
|
||||
}
|
||||
}
|
||||
|
||||
ResponsiveImage {
|
||||
id: icon
|
||||
|
||||
// TODO: remove this when the icons are size corrected
|
||||
property real size: ItemAction.size !== undefined ?
|
||||
ItemAction.size : 30
|
||||
containerWidth: size
|
||||
containerHeight: size
|
||||
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
source: ItemAction.icon.source
|
||||
color: ItemAction.icon.color
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
running: ItemAction.blinksWhenChecked !== undefined &&
|
||||
ItemAction.blinksWhenChecked && checked
|
||||
NumberAnimation { from: 1; to: 0; duration: JamiTheme.recordBlinkDuration }
|
||||
NumberAnimation { from: 0; to: 1; duration: JamiTheme.recordBlinkDuration }
|
||||
}
|
||||
}
|
||||
|
||||
// custom anchor for the tooltips
|
||||
Item {
|
||||
anchors.top: !isVertical ? parent.bottom : undefined
|
||||
anchors.topMargin: 25
|
||||
anchors.horizontalCenter: !isVertical ? parent.horizontalCenter : undefined
|
||||
|
||||
anchors.right: isVertical ? parent.left : undefined
|
||||
anchors.rightMargin: isVertical ? toolTip.contentWidth / 2 + 12 : 0
|
||||
anchors.verticalCenter: isVertical ? parent.verticalCenter : undefined
|
||||
anchors.verticalCenterOffset: isVertical ? toolTip.contentHeight / 2 + 4 : 0
|
||||
|
||||
MaterialToolTip {
|
||||
id: toolTip
|
||||
parent: parent
|
||||
visible: text.length > 0 && (wrapper.hovered || menu.hovered)
|
||||
text: menu.hovered ? menuAction.text : ItemAction.text
|
||||
verticalPadding: 1
|
||||
font.pointSize: 9
|
||||
}
|
||||
}
|
||||
|
||||
property var menuAction: ItemAction.menuAction
|
||||
|
||||
ComboBox {
|
||||
id: menu
|
||||
|
||||
indicator: null
|
||||
|
||||
visible: menuAction !== undefined && !BadgeCount
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 18
|
||||
height: width
|
||||
y: -4
|
||||
|
||||
Connections {
|
||||
target: menuAction !== undefined ?
|
||||
menuAction :
|
||||
null
|
||||
function onTriggered() {
|
||||
itemListView.currentIndex =
|
||||
menuAction.listModel.getCurrentIndex()
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: "^"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: "white"
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: menu.down ?
|
||||
"#aaaaaa" :
|
||||
menu.hovered ?
|
||||
"#777777" :
|
||||
"#444444"
|
||||
radius: 4
|
||||
}
|
||||
|
||||
onActivated: menuAction.accept(index)
|
||||
model: visible ? menuAction.listModel : null
|
||||
delegate: ItemDelegate {
|
||||
id: menuItem
|
||||
|
||||
width: itemListView.menuItemWidth
|
||||
height: itemListView.menuItemHeight
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
color: menuItem.down ?
|
||||
"#c4aaaaaa" :
|
||||
menuItem.hovered ?
|
||||
"#c4777777" :
|
||||
"transparent"
|
||||
}
|
||||
contentItem: RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
ResponsiveImage {
|
||||
source: menuItem.ListView.isCurrentItem ?
|
||||
"qrc:/images/icons/check_box-24px.svg" :
|
||||
"qrc:/images/icons/check_box_outline_blank-24px.svg"
|
||||
layer.enabled: true
|
||||
layer.effect: ColorOverlay { color: "white" }
|
||||
}
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: DeviceName
|
||||
elide: Text.ElideRight
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
popup: Popup {
|
||||
id: itemPopup
|
||||
|
||||
y: isVertical ?
|
||||
-(implicitHeight - wrapper.height) / 2 :
|
||||
-implicitHeight - 12
|
||||
x: isVertical ?
|
||||
-implicitWidth - 24 :
|
||||
-(implicitWidth - wrapper.width) / 2 - 18
|
||||
|
||||
implicitWidth: contentItem.implicitWidth
|
||||
implicitHeight: contentItem.implicitHeight
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
|
||||
onOpened: menuAction.triggered()
|
||||
|
||||
contentItem: ListView {
|
||||
id: itemListView
|
||||
|
||||
property real menuItemWidth: 0
|
||||
property real menuItemHeight: 39
|
||||
|
||||
orientation: ListView.Vertical
|
||||
implicitWidth: menuItemWidth
|
||||
implicitHeight: Math.min(contentHeight,
|
||||
menuItemHeight * 6) + 24
|
||||
|
||||
ScrollIndicator.vertical: ScrollIndicator {}
|
||||
|
||||
clip: true
|
||||
|
||||
model: menu.delegateModel
|
||||
|
||||
TextMetrics { id: itemTextMetrics }
|
||||
|
||||
// recalc list width based on max item width
|
||||
onCountChanged: {
|
||||
// Hack: use AudioDeviceModel.DeviceName role for video as well
|
||||
var maxWidth = 0
|
||||
for (var i = 0; i < count; ++i) {
|
||||
var idx = menuAction.listModel.index(i, 0)
|
||||
itemTextMetrics.text = menuAction.listModel.data(
|
||||
idx, AudioDeviceModel.DeviceName)
|
||||
if (itemTextMetrics.boundingRect.width > maxWidth)
|
||||
maxWidth = itemTextMetrics.boundingRect.width
|
||||
}
|
||||
// 30(icon) + 5(layout spacing) + 12(margins)
|
||||
menuItemWidth = Math.min(256, maxWidth + 30 + 5 + 12)
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 5
|
||||
color: "#c4272727"
|
||||
}
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: DropShadow {
|
||||
z: -1
|
||||
horizontalOffset: 0
|
||||
verticalOffset: 0
|
||||
radius: 8.0
|
||||
samples: 16
|
||||
color: "#80000000"
|
||||
}
|
||||
}
|
||||
|
||||
BadgeNotifier {
|
||||
id: badge
|
||||
|
||||
count: BadgeCount
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 18
|
||||
height: width
|
||||
radius: 4
|
||||
y: -4
|
||||
}
|
||||
}
|
|
@ -29,6 +29,8 @@ import net.jami.Adapters 1.0
|
|||
import net.jami.Constants 1.0
|
||||
|
||||
import "../js/contactpickercreation.js" as ContactPickerCreation
|
||||
import "../js/selectscreenwindowcreation.js" as SelectScreenWindowCreation
|
||||
import "../js/screenrubberbandcreation.js" as ScreenRubberBandCreation
|
||||
import "../js/pluginhandlerpickercreation.js" as PluginHandlerPickerCreation
|
||||
|
||||
import "../../commoncomponents"
|
||||
|
@ -37,13 +39,18 @@ Item {
|
|||
id: root
|
||||
|
||||
property alias participantsLayer: __participantsLayer
|
||||
property string timeText: "00:00"
|
||||
property string remoteRecordingLabel: ""
|
||||
property bool isVideoMuted: true
|
||||
property bool isAudioOnly: false
|
||||
|
||||
property bool isPaused
|
||||
property bool isAudioOnly
|
||||
property bool isAudioMuted
|
||||
property bool isVideoMuted
|
||||
property bool isRecording
|
||||
property bool isSIP
|
||||
property bool isModerator
|
||||
|
||||
property string bestName: ""
|
||||
|
||||
signal overlayChatButtonClicked
|
||||
signal chatButtonClicked
|
||||
|
||||
onVisibleChanged: if (!visible) callViewContextMenu.close()
|
||||
|
||||
|
@ -54,25 +61,23 @@ Item {
|
|||
|
||||
function setRecording(localIsRecording) {
|
||||
callViewContextMenu.localIsRecording = localIsRecording
|
||||
recordingRect.visible = localIsRecording
|
||||
mainOverlay.recordingVisible = localIsRecording
|
||||
|| callViewContextMenu.peerIsRecording
|
||||
}
|
||||
|
||||
function updateButtonStatus(isPaused, isAudioOnly, isAudioMuted, isVideoMuted,
|
||||
isRecording, isSIP, isConferenceCall) {
|
||||
function updateUI(isPaused, isAudioOnly, isAudioMuted,
|
||||
isVideoMuted, isRecording, isSIP) {
|
||||
if (isPaused !== undefined) {
|
||||
root.isPaused = isPaused
|
||||
root.isAudioOnly = isAudioOnly
|
||||
root.isAudioMuted = isAudioMuted
|
||||
root.isVideoMuted = isVideoMuted
|
||||
callViewContextMenu.isSIP = isSIP
|
||||
callViewContextMenu.isPaused = isPaused
|
||||
callViewContextMenu.isAudioOnly = isAudioOnly
|
||||
callViewContextMenu.localIsRecording = isRecording
|
||||
recordingRect.visible = isRecording
|
||||
callOverlayButtonGroup.setButtonStatus(isPaused, isAudioOnly,
|
||||
isAudioMuted, isVideoMuted,
|
||||
isSIP, isConferenceCall)
|
||||
root.isRecording = isRecording
|
||||
root.isSIP = isSIP
|
||||
mainOverlay.recordingVisible = isRecording
|
||||
}
|
||||
|
||||
function updateMenu() {
|
||||
callOverlayButtonGroup.updateMenu()
|
||||
root.isModerator = CallAdapter.isCurrentModerator()
|
||||
}
|
||||
|
||||
function showOnHoldImage(visible) {
|
||||
|
@ -109,17 +114,19 @@ Item {
|
|||
: JamiStrings.isRecording)
|
||||
}
|
||||
|
||||
remoteRecordingLabel = state? label : JamiStrings.peerStoppedRecording
|
||||
mainOverlay.remoteRecordingLabel = state ?
|
||||
label :
|
||||
JamiStrings.peerStoppedRecording
|
||||
callViewContextMenu.peerIsRecording = state
|
||||
recordingRect.visible = callViewContextMenu.localIsRecording
|
||||
mainOverlay.recordingVisible = callViewContextMenu.localIsRecording
|
||||
|| callViewContextMenu.peerIsRecording
|
||||
callOverlayRectMouseArea.entered()
|
||||
}
|
||||
|
||||
function resetRemoteRecording() {
|
||||
remoteRecordingLabel = ""
|
||||
mainOverlay.remoteRecordingLabel = ""
|
||||
callViewContextMenu.peerIsRecording = false
|
||||
recordingRect.visible = callViewContextMenu.localIsRecording
|
||||
mainOverlay.recordingVisible = callViewContextMenu.localIsRecording
|
||||
}
|
||||
|
||||
SipInputPanel {
|
||||
|
@ -143,182 +150,61 @@ Item {
|
|||
source: "qrc:/images/icons/ic_pause_white_100px.svg"
|
||||
}
|
||||
|
||||
Item {
|
||||
function openContactPicker(type) {
|
||||
ContactPickerCreation.createContactPickerObjects(type, root)
|
||||
ContactPickerCreation.openContactPicker()
|
||||
}
|
||||
|
||||
function openShareScreen() {
|
||||
if (Qt.application.screens.length === 1) {
|
||||
AvAdapter.shareEntireScreen(0)
|
||||
} else {
|
||||
SelectScreenWindowCreation.createSelectScreenWindowObject()
|
||||
SelectScreenWindowCreation.showSelectScreenWindow()
|
||||
}
|
||||
}
|
||||
|
||||
function openShareScreenArea() {
|
||||
if (Qt.platform.os !== "windows") {
|
||||
AvAdapter.shareScreenArea(0, 0, 0, 0)
|
||||
} else {
|
||||
ScreenRubberBandCreation.createScreenRubberBandWindowObject()
|
||||
ScreenRubberBandCreation.showScreenRubberBandWindow()
|
||||
}
|
||||
}
|
||||
|
||||
function openPluginsMenu() {
|
||||
PluginHandlerPickerCreation.createPluginHandlerPickerObjects(root, true)
|
||||
PluginHandlerPickerCreation.openPluginHandlerPicker()
|
||||
}
|
||||
|
||||
MainOverlay {
|
||||
id: mainOverlay
|
||||
|
||||
anchors.fill: parent
|
||||
opacity: 0
|
||||
|
||||
// (un)subscribe to an app-wide mouse move event trap filtered
|
||||
// for the overlay's geometry
|
||||
onVisibleChanged: visible ?
|
||||
CallOverlayModel.registerFilter(appWindow, this) :
|
||||
CallOverlayModel.unregisterFilter(appWindow, this)
|
||||
bestName: root.bestName
|
||||
|
||||
Connections {
|
||||
target: CallOverlayModel
|
||||
|
||||
function onMouseMoved(item) {
|
||||
if (item === mainOverlay) {
|
||||
mainOverlay.opacity = 1
|
||||
fadeOutTimer.restart()
|
||||
target: mainOverlay.callActionBar
|
||||
function onChatClicked() { root.chatButtonClicked() }
|
||||
function onAddToConferenceClicked() { openContactPicker(ContactList.CONFERENCE) }
|
||||
function onTransferClicked() { openContactPicker(ContactList.TRANSFER) }
|
||||
function onShareScreenClicked() { openShareScreen() }
|
||||
function onShareScreenAreaClicked() { openShareScreenArea() }
|
||||
function onPluginsClicked() { openPluginsMenu() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// control overlay fade out.
|
||||
Timer {
|
||||
id: fadeOutTimer
|
||||
interval: JamiTheme.overlayFadeDelay
|
||||
onTriggered: {
|
||||
if (callOverlayButtonGroup.hovered)
|
||||
return
|
||||
mainOverlay.opacity = 0
|
||||
resetLabelsTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to reset recording label and call duration time
|
||||
Timer {
|
||||
id: resetLabelsTimer
|
||||
interval: 1000
|
||||
running: root.visible
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
timeText = CallAdapter.getCallDurationTime(LRCInstance.currentAccountId,
|
||||
LRCInstance.selectedConvUid)
|
||||
if (mainOverlay.opacity === 0 && !callViewContextMenu.peerIsRecording)
|
||||
remoteRecordingLabel = ""
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: overlayUpperPartRect
|
||||
|
||||
anchors.top: parent.top
|
||||
|
||||
width: parent.width
|
||||
height: 50
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
Text {
|
||||
id: jamiBestNameText
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||
Layout.preferredWidth: overlayUpperPartRect.width / 3
|
||||
Layout.preferredHeight: 50
|
||||
leftPadding: 16
|
||||
|
||||
font.pointSize: JamiTheme.textFontSize
|
||||
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
text: textMetricsjamiBestNameText.elidedText
|
||||
color: "white"
|
||||
|
||||
TextMetrics {
|
||||
id: textMetricsjamiBestNameText
|
||||
font: jamiBestNameText.font
|
||||
text: {
|
||||
if (!root.isAudioOnly) {
|
||||
if (remoteRecordingLabel === "") {
|
||||
return root.bestName
|
||||
} else {
|
||||
return remoteRecordingLabel
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
elideWidth: overlayUpperPartRect.width / 3
|
||||
elide: Qt.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: callTimerText
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
Layout.preferredWidth: 64
|
||||
Layout.minimumWidth: 64
|
||||
Layout.preferredHeight: 48
|
||||
Layout.rightMargin: recordingRect.visible?
|
||||
0 : JamiTheme.preferredMarginSize
|
||||
font.pointSize: JamiTheme.textFontSize
|
||||
horizontalAlignment: Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: textMetricscallTimerText.elidedText
|
||||
color: "white"
|
||||
TextMetrics {
|
||||
id: textMetricscallTimerText
|
||||
font: callTimerText.font
|
||||
text: timeText
|
||||
elideWidth: overlayUpperPartRect.width / 4
|
||||
elide: Qt.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: recordingRect
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
Layout.rightMargin: JamiTheme.preferredMarginSize
|
||||
height: 16
|
||||
width: 16
|
||||
radius: height / 2
|
||||
color: "red"
|
||||
|
||||
SequentialAnimation on color {
|
||||
loops: Animation.Infinite
|
||||
running: true
|
||||
ColorAnimation { from: "red"; to: "transparent"; duration: 500 }
|
||||
ColorAnimation { from: "transparent"; to: "red"; duration: 500 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CallOverlayButtonGroup {
|
||||
id: callOverlayButtonGroup
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 10
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
height: 56
|
||||
width: root.width
|
||||
|
||||
onChatButtonClicked: {
|
||||
root.overlayChatButtonClicked()
|
||||
}
|
||||
|
||||
onAddToConferenceButtonClicked: {
|
||||
// Create contact picker - conference.
|
||||
ContactPickerCreation.createContactPickerObjects(
|
||||
ContactList.CONFERENCE,
|
||||
root)
|
||||
ContactPickerCreation.openContactPicker()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: JamiTheme.overlayFadeDuration }}
|
||||
}
|
||||
|
||||
CallViewContextMenu {
|
||||
id: callViewContextMenu
|
||||
|
||||
onTransferCallButtonClicked: {
|
||||
// Create contact picker - sip transfer.
|
||||
ContactPickerCreation.createContactPickerObjects(
|
||||
ContactList.TRANSFER,
|
||||
root)
|
||||
ContactPickerCreation.openContactPicker()
|
||||
}
|
||||
isSIP: root.isSIP
|
||||
isPaused: root.isPaused
|
||||
isAudioOnly: root.isAudioOnly
|
||||
localIsRecording: root.isRecording
|
||||
|
||||
onPluginItemClicked: {
|
||||
// Create plugin handler picker - PLUGINS
|
||||
PluginHandlerPickerCreation.createPluginHandlerPickerObjects(root, true)
|
||||
PluginHandlerPickerCreation.openPluginHandlerPicker()
|
||||
}
|
||||
onTransferCallButtonClicked: openContactPicker(ContactList.TRANSFER)
|
||||
onPluginItemClicked: openPluginsMenu()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,211 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2020 by Savoir-faire Linux
|
||||
* Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
|
||||
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
|
||||
*
|
||||
* 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 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
import QtQuick.Controls.Universal 2.14
|
||||
import QtQml 2.14
|
||||
|
||||
import net.jami.Adapters 1.0
|
||||
import net.jami.Models 1.0
|
||||
import net.jami.Constants 1.0
|
||||
|
||||
import "../../commoncomponents"
|
||||
|
||||
Control {
|
||||
id: root
|
||||
|
||||
// ButtonCounts here is to make sure that flow layout margin is calculated correctly,
|
||||
// since no other methods can make buttons at the layout center.
|
||||
property var isModerator: true
|
||||
property var isSip: false
|
||||
|
||||
signal chatButtonClicked
|
||||
signal addToConferenceButtonClicked
|
||||
|
||||
function updateMenu() {
|
||||
root.isModerator = CallAdapter.isCurrentModerator()
|
||||
addToConferenceButton.visible = !root.isSip && root.isModerator
|
||||
}
|
||||
|
||||
function setButtonStatus(isPaused, isAudioOnly, isAudioMuted, isVideoMuted,
|
||||
isSIP, isConferenceCall) {
|
||||
root.isModerator = CallAdapter.isCurrentModerator()
|
||||
root.isSip = isSIP
|
||||
noVideoButton.visible = !isAudioOnly
|
||||
addToConferenceButton.visible = !root.isSIP && root.isModerator
|
||||
|
||||
noMicButton.checked = isAudioMuted
|
||||
noVideoButton.checked = isVideoMuted
|
||||
}
|
||||
|
||||
z: 2
|
||||
|
||||
RowLayout {
|
||||
id: callOverlayButtonGroup
|
||||
|
||||
spacing: 8
|
||||
height: 56
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: {
|
||||
// TODO: refactor with Flow if possible
|
||||
// 6 is the number of button
|
||||
// If ~ 500px, go into wide mode
|
||||
if (callOverlayButtonGroup.width < (JamiTheme.callButtonPreferredSize * 6 -
|
||||
callOverlayButtonGroup.spacing * 6 + 300)) {
|
||||
return 0
|
||||
} else {
|
||||
return callOverlayButtonGroup.width / 2 - JamiTheme.callButtonPreferredSize * 1.5 -
|
||||
callOverlayButtonGroup.spacing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PushButton {
|
||||
id: noMicButton
|
||||
|
||||
Layout.leftMargin: 8
|
||||
Layout.preferredWidth: JamiTheme.callButtonPreferredSize
|
||||
Layout.preferredHeight: JamiTheme.callButtonPreferredSize
|
||||
|
||||
pressedColor: JamiTheme.invertedPressedButtonColor
|
||||
hoveredColor: JamiTheme.invertedHoveredButtonColor
|
||||
normalColor: JamiTheme.invertedNormalButtonColor
|
||||
|
||||
normalImageSource: "qrc:/images/icons/mic-24px.svg"
|
||||
imageColor: JamiTheme.whiteColor
|
||||
checkable: true
|
||||
checkedImageSource: "qrc:/images/icons/mic_off-24px.svg"
|
||||
checkedImageColor: JamiTheme.declineButtonPressedRed
|
||||
|
||||
toolTipText: !checked ? JamiStrings.mute : JamiStrings.unmute
|
||||
|
||||
onClicked: CallAdapter.muteThisCallToggle()
|
||||
}
|
||||
|
||||
PushButton {
|
||||
id: hangUpButton
|
||||
|
||||
Layout.preferredWidth: JamiTheme.callButtonPreferredSize
|
||||
Layout.preferredHeight: JamiTheme.callButtonPreferredSize
|
||||
|
||||
pressedColor: JamiTheme.declineButtonPressedRed
|
||||
hoveredColor: JamiTheme.declineButtonHoverRed
|
||||
normalColor: JamiTheme.declineButtonRed
|
||||
|
||||
source: "qrc:/images/icons/ic_call_end_white_24px.svg"
|
||||
imageColor: JamiTheme.whiteColor
|
||||
|
||||
toolTipText: JamiStrings.hangup
|
||||
|
||||
onClicked: CallAdapter.hangUpThisCall()
|
||||
}
|
||||
|
||||
PushButton {
|
||||
id: noVideoButton
|
||||
|
||||
Layout.preferredWidth: JamiTheme.callButtonPreferredSize
|
||||
Layout.preferredHeight: JamiTheme.callButtonPreferredSize
|
||||
|
||||
pressedColor: JamiTheme.invertedPressedButtonColor
|
||||
hoveredColor: JamiTheme.invertedHoveredButtonColor
|
||||
normalColor: JamiTheme.invertedNormalButtonColor
|
||||
|
||||
normalImageSource: "qrc:/images/icons/videocam-24px.svg"
|
||||
imageColor: JamiTheme.whiteColor
|
||||
checkable: true
|
||||
checkedImageSource: "qrc:/images/icons/videocam_off-24px.svg"
|
||||
checkedImageColor: JamiTheme.declineButtonPressedRed
|
||||
|
||||
toolTipText: !checked ? JamiStrings.pauseVideo : JamiStrings.resumeVideo
|
||||
|
||||
onClicked: CallAdapter.videoPauseThisCallToggle()
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
PushButton {
|
||||
id: addToConferenceButton
|
||||
|
||||
Layout.preferredWidth: JamiTheme.callButtonPreferredSize
|
||||
Layout.preferredHeight: JamiTheme.callButtonPreferredSize
|
||||
visible: !isModerator && !isSip
|
||||
|
||||
pressedColor: JamiTheme.invertedPressedButtonColor
|
||||
hoveredColor: JamiTheme.invertedHoveredButtonColor
|
||||
normalColor: JamiTheme.invertedNormalButtonColor
|
||||
|
||||
source: "qrc:/images/icons/group_add-24px.svg"
|
||||
imageColor: JamiTheme.whiteColor
|
||||
|
||||
toolTipText: JamiStrings.addParticipants
|
||||
|
||||
onClicked: root.addToConferenceButtonClicked()
|
||||
}
|
||||
|
||||
PushButton {
|
||||
id: chatButton
|
||||
|
||||
Layout.preferredWidth: JamiTheme.callButtonPreferredSize
|
||||
Layout.preferredHeight: JamiTheme.callButtonPreferredSize
|
||||
|
||||
pressedColor: JamiTheme.invertedPressedButtonColor
|
||||
hoveredColor: JamiTheme.invertedHoveredButtonColor
|
||||
normalColor: JamiTheme.invertedNormalButtonColor
|
||||
|
||||
source: "qrc:/images/icons/chat-24px.svg"
|
||||
imageColor: JamiTheme.whiteColor
|
||||
|
||||
toolTipText: JamiStrings.chat
|
||||
|
||||
onClicked: root.chatButtonClicked()
|
||||
}
|
||||
|
||||
PushButton {
|
||||
id: optionsButton
|
||||
|
||||
Layout.preferredWidth: JamiTheme.callButtonPreferredSize
|
||||
Layout.preferredHeight: JamiTheme.callButtonPreferredSize
|
||||
Layout.rightMargin: 8
|
||||
|
||||
pressedColor: JamiTheme.invertedPressedButtonColor
|
||||
hoveredColor: JamiTheme.invertedHoveredButtonColor
|
||||
normalColor: JamiTheme.invertedNormalButtonColor
|
||||
|
||||
source: "qrc:/images/icons/more_vert-24px.svg"
|
||||
imageColor: JamiTheme.whiteColor
|
||||
|
||||
toolTipText: JamiStrings.moreOptions
|
||||
|
||||
onClicked: {
|
||||
var rectPos = mapToItem(callStackViewWindow, optionsButton.x, optionsButton.y)
|
||||
callViewContextMenu.x = rectPos.x + optionsButton.width / 2
|
||||
- callViewContextMenu.width / 2
|
||||
callViewContextMenu.y = rectPos.y - 12 - callViewContextMenu.height
|
||||
callViewContextMenu.openMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
200
src/mainview/components/MainOverlay.qml
Normal file
200
src/mainview/components/MainOverlay.qml
Normal file
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* Copyright (C) 2020-2021 by Savoir-faire Linux
|
||||
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
|
||||
* Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
|
||||
* Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com>
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
*
|
||||
* 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 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
import QtQuick.Controls.Universal 2.14
|
||||
import QtQml 2.14
|
||||
|
||||
import net.jami.Models 1.0
|
||||
import net.jami.Adapters 1.0
|
||||
import net.jami.Constants 1.0
|
||||
|
||||
import "../../commoncomponents"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string timeText: "00:00"
|
||||
property string remoteRecordingLabel: ""
|
||||
|
||||
property string bestName: ""
|
||||
|
||||
property alias recordingVisible: recordingRect.visible
|
||||
property alias callActionBar: __callActionBar
|
||||
|
||||
property bool frozen: callActionBar.overflowOpen ||
|
||||
callActionBar.hovered ||
|
||||
callActionBar.subMenuOpen
|
||||
|
||||
opacity: 0
|
||||
|
||||
// (un)subscribe to an app-wide mouse move event trap filtered
|
||||
// for the overlay's geometry
|
||||
onVisibleChanged: visible ?
|
||||
CallOverlayModel.registerFilter(appWindow, this) :
|
||||
CallOverlayModel.unregisterFilter(appWindow, this)
|
||||
|
||||
Connections {
|
||||
target: CallOverlayModel
|
||||
|
||||
function onMouseMoved(item) {
|
||||
if (item === root) {
|
||||
root.opacity = 1
|
||||
fadeOutTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// control overlay fade out.
|
||||
Timer {
|
||||
id: fadeOutTimer
|
||||
interval: JamiTheme.overlayFadeDelay
|
||||
onTriggered: {
|
||||
if (frozen)
|
||||
return
|
||||
root.opacity = 0
|
||||
resetLabelsTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to reset recording label and call duration time
|
||||
Timer {
|
||||
id: resetLabelsTimer
|
||||
interval: 1000
|
||||
running: root.visible
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
root.timeText = CallAdapter.getCallDurationTime(
|
||||
LRCInstance.currentAccountId,
|
||||
LRCInstance.selectedConvUid)
|
||||
if (root.opacity === 0 && !callViewContextMenu.peerIsRecording)
|
||||
root.remoteRecordingLabel = ""
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: overlayUpperPartRect
|
||||
|
||||
anchors.top: parent.top
|
||||
|
||||
width: parent.width
|
||||
height: 50
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
Text {
|
||||
id: jamiBestNameText
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||
Layout.preferredWidth: overlayUpperPartRect.width / 3
|
||||
Layout.preferredHeight: 50
|
||||
leftPadding: 16
|
||||
|
||||
font.pointSize: JamiTheme.textFontSize
|
||||
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
text: textMetricsjamiBestNameText.elidedText
|
||||
color: "white"
|
||||
|
||||
TextMetrics {
|
||||
id: textMetricsjamiBestNameText
|
||||
font: jamiBestNameText.font
|
||||
text: {
|
||||
if (!root.isAudioOnly) {
|
||||
if (remoteRecordingLabel === "") {
|
||||
return root.bestName
|
||||
} else {
|
||||
return remoteRecordingLabel
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
elideWidth: overlayUpperPartRect.width / 3
|
||||
elide: Qt.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: callTimerText
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
Layout.preferredWidth: 64
|
||||
Layout.minimumWidth: 64
|
||||
Layout.preferredHeight: 48
|
||||
Layout.rightMargin: recordingRect.visible?
|
||||
0 : JamiTheme.preferredMarginSize
|
||||
font.pointSize: JamiTheme.textFontSize
|
||||
horizontalAlignment: Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: textMetricscallTimerText.elidedText
|
||||
color: "white"
|
||||
TextMetrics {
|
||||
id: textMetricscallTimerText
|
||||
font: callTimerText.font
|
||||
text: timeText
|
||||
elideWidth: overlayUpperPartRect.width / 4
|
||||
elide: Qt.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: recordingRect
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
Layout.rightMargin: JamiTheme.preferredMarginSize
|
||||
height: 16
|
||||
width: 16
|
||||
radius: height / 2
|
||||
color: "red"
|
||||
|
||||
SequentialAnimation on color {
|
||||
loops: Animation.Infinite
|
||||
running: true
|
||||
ColorAnimation {
|
||||
from: "red"; to: "transparent";
|
||||
duration: JamiTheme.recordBlinkDuration
|
||||
}
|
||||
ColorAnimation {
|
||||
from: "transparent"; to: "red";
|
||||
duration: JamiTheme.recordBlinkDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CallActionBar {
|
||||
id: __callActionBar
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 26
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
height: 55
|
||||
}
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: JamiTheme.overlayFadeDuration }}
|
||||
}
|
|
@ -302,15 +302,11 @@ Rectangle {
|
|||
target: CallAdapter
|
||||
|
||||
function onUpdateOverlay(isPaused, isAudioOnly, isAudioMuted, isVideoMuted,
|
||||
isRecording, isSIP, isConferenceCall, bestName) {
|
||||
isRecording, isSIP, bestName) {
|
||||
callOverlay.showOnHoldImage(isPaused)
|
||||
audioCallPageRectCentralRect.visible = !isPaused && root.isAudioOnly
|
||||
callOverlay.updateButtonStatus(isPaused,
|
||||
isAudioOnly,
|
||||
isAudioMuted,
|
||||
isVideoMuted,
|
||||
isRecording, isSIP,
|
||||
isConferenceCall)
|
||||
callOverlay.updateUI(isPaused, isAudioOnly, isAudioMuted, isVideoMuted,
|
||||
isRecording, isSIP)
|
||||
root.bestName = bestName
|
||||
callOverlay.participantsLayer.update(CallAdapter.getConferencesInfos())
|
||||
}
|
||||
|
@ -329,7 +325,7 @@ Rectangle {
|
|||
}
|
||||
}
|
||||
|
||||
onOverlayChatButtonClicked: {
|
||||
onChatButtonClicked: {
|
||||
inCallMessageWebViewStack.visible ?
|
||||
closeInCallConversation() :
|
||||
openInCallConversation()
|
||||
|
|
|
@ -40,7 +40,7 @@ Item {
|
|||
// TODO: in the future the conference layout should be entirely managed by the client
|
||||
// Hack: truncate and ceil participant's overlay position and size to correct
|
||||
// when they are not exacts
|
||||
callOverlay.updateMenu()
|
||||
callOverlay.updateUI()
|
||||
var showMax = false
|
||||
var showMin = false
|
||||
|
||||
|
|
|
@ -50,6 +50,6 @@ GeneralMenuItem {
|
|||
|
||||
onClicked: {
|
||||
var deviceName = videoCallPageContextMenuDeviceItem.itemName
|
||||
AvAdapter.onVideoContextMenuDeviceItemClicked(deviceName)
|
||||
AvAdapter.selectVideoInputDeviceByName(deviceName)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ ColumnLayout {
|
|||
}
|
||||
|
||||
deviceComboBoxSetting.setCurrentIndex(
|
||||
deviceComboBoxSetting.comboModel.getCurrentSettingIndex(), true)
|
||||
deviceComboBoxSetting.comboModel.getCurrentIndex(), true)
|
||||
hardwareAccelControl.checked = AVModel.getHardwareAcceleration()
|
||||
}
|
||||
|
||||
|
|
|
@ -97,6 +97,8 @@ VideoInputDeviceModel::data(const QModelIndex& index, int role) const
|
|||
return QVariant((QString) currentDeviceSetting.size);
|
||||
case Role::DeviceName_UTF8:
|
||||
return QVariant(currentDeviceSetting.name.toUtf8());
|
||||
case Role::isCurrent:
|
||||
return QVariant(index.row() == getCurrentIndex());
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
@ -111,6 +113,7 @@ VideoInputDeviceModel::roleNames() const
|
|||
roles[CurrentFrameRate] = "CurrentFrameRate";
|
||||
roles[CurrentResolution] = "CurrentResolution";
|
||||
roles[DeviceName_UTF8] = "DeviceName_UTF8";
|
||||
roles[isCurrent] = "isCurrent";
|
||||
return roles;
|
||||
}
|
||||
|
||||
|
@ -159,15 +162,9 @@ VideoInputDeviceModel::deviceCount()
|
|||
}
|
||||
|
||||
int
|
||||
VideoInputDeviceModel::getCurrentSettingIndex()
|
||||
VideoInputDeviceModel::getCurrentIndex() const
|
||||
{
|
||||
QString currentId = lrcInstance_->avModel().getCurrentVideoCaptureDevice();
|
||||
auto resultList = match(index(0, 0), DeviceId, QVariant(currentId));
|
||||
|
||||
int resultRowIndex = 0;
|
||||
if (resultList.size() > 0) {
|
||||
resultRowIndex = resultList[0].row();
|
||||
}
|
||||
|
||||
return resultRowIndex;
|
||||
return resultList.size() > 0 ? resultList[0].row() : 0;
|
||||
}
|
||||
|
|
|
@ -25,12 +25,13 @@ class VideoInputDeviceModel : public AbstractListModelBase
|
|||
Q_OBJECT
|
||||
public:
|
||||
enum Role {
|
||||
DeviceChannel = Qt::UserRole + 1,
|
||||
DeviceName,
|
||||
DeviceName = Qt::UserRole + 1,
|
||||
DeviceChannel,
|
||||
DeviceId,
|
||||
CurrentFrameRate,
|
||||
CurrentResolution,
|
||||
DeviceName_UTF8
|
||||
DeviceName_UTF8,
|
||||
isCurrent
|
||||
};
|
||||
Q_ENUM(Role)
|
||||
|
||||
|
@ -59,8 +60,7 @@ public:
|
|||
* This function is to reset the model when there's new account added.
|
||||
*/
|
||||
Q_INVOKABLE int deviceCount();
|
||||
/*
|
||||
* This function is to get the current device id in the demon.
|
||||
*/
|
||||
Q_INVOKABLE int getCurrentSettingIndex();
|
||||
|
||||
// get model index of the current device
|
||||
Q_INVOKABLE int getCurrentIndex() const;
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue