Compare commits
61 Commits
Author | SHA1 | Date | |
---|---|---|---|
f4d806f5fc | |||
4854b6151d | |||
c755b4a52a | |||
7505b06ddf | |||
ace663804e | |||
2ff41313f8 | |||
1e1772e306 | |||
300b28bdfa | |||
1f4e81af35 | |||
335d646c42 | |||
b6f5123495 | |||
fbe0b1f819 | |||
000a4c7920 | |||
262714d3ee | |||
d06982b38a | |||
c21e39b158 | |||
8d0426f775 | |||
2c031fce3f | |||
6e1b8a9f17 | |||
4b85401adf | |||
5932d8cb84 | |||
adf6cefd1f | |||
142255ccc8 | |||
1b6b8e043a | |||
1b4c211c1d | |||
43c71ec1a5 | |||
4d4fd21fe9 | |||
6cbacef95b | |||
7a817eb82a | |||
49fc253c19 | |||
df5a1a901a | |||
54a2da4670 | |||
361f1f0e29 | |||
9031a4a3e3 | |||
0a378c1682 | |||
8bc4613407 | |||
ec6c04a7df | |||
769119c795 | |||
d1e90c6aef | |||
6d705deb55 | |||
464fba23c5 | |||
c60808a7da | |||
a2273e8c27 | |||
a20a00130d | |||
d0e2f61d03 | |||
8ea1a77186 | |||
bf1bea1e93 | |||
124decc34a | |||
89caef6905 | |||
9118e01775 | |||
138135b9e9 | |||
2863eb790d | |||
06e8c79b3f | |||
81695737cd | |||
ac07cb529f | |||
4f77e2c105 | |||
47ce9252b7 | |||
9153836ead | |||
f8a7087779 | |||
01546f0047 | |||
d8dd16e865 |
3
.gitignore
vendored
@ -6,6 +6,7 @@ tests/tests
|
||||
tests/libs
|
||||
tests/.cache
|
||||
tests/__pycache__
|
||||
tests/avatars
|
||||
toxygen/libs
|
||||
.idea
|
||||
*~
|
||||
@ -23,4 +24,4 @@ toxygen/__pycache__
|
||||
html
|
||||
Toxygen.egg-info
|
||||
*.tox
|
||||
|
||||
.cache
|
||||
|
12
.travis.yml
@ -2,16 +2,24 @@ language: python
|
||||
python:
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
os:
|
||||
- linux
|
||||
dist: trusty
|
||||
notifications:
|
||||
email: false
|
||||
before_install:
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install -y checkinstall build-essential
|
||||
- sudo apt-get install portaudio19-dev
|
||||
- sudo apt-get install libsecret-1-dev
|
||||
- sudo apt-get install libconfig-dev libvpx-dev check -qq
|
||||
- sudo apt-get install -y python3-pyqt5
|
||||
install:
|
||||
- pip install sip
|
||||
- pip install pyqt5
|
||||
- pip install pyaudio
|
||||
- pip install opencv-python
|
||||
before_script:
|
||||
# OPUS
|
||||
# Opus
|
||||
- wget http://downloads.xiph.org/releases/opus/opus-1.0.3.tar.gz
|
||||
- tar xzf opus-1.0.3.tar.gz
|
||||
- cd opus-1.0.3
|
||||
|
@ -12,9 +12,7 @@ include toxygen/smileys/starwars/*.png
|
||||
include toxygen/smileys/starwars/config.json
|
||||
include toxygen/smileys/ksk/*.png
|
||||
include toxygen/smileys/ksk/config.json
|
||||
include toxygen/styles/style.qss
|
||||
include toxygen/styles/*.qss
|
||||
include toxygen/translations/*.qm
|
||||
include toxygen/libs/libtox.dll
|
||||
include toxygen/libs/libsodium.a
|
||||
include toxygen/libs/libtox64.dll
|
||||
include toxygen/libs/libsodium64.a
|
61
README.md
@ -10,41 +10,38 @@ Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pu
|
||||
|
||||
### [Install](/docs/install.md) - [Contribute](/docs/contributing.md) - [Plugins](/docs/plugins.md) - [Compile](/docs/compile.md) - [Contact](/docs/contact.md) - [Updater](https://github.com/toxygen-project/toxygen_updater)
|
||||
|
||||
### Supported OS:
|
||||
|
||||

|
||||
### Supported OS: Linux and Windows
|
||||
|
||||
### Features:
|
||||
|
||||
- [x] 1v1 messages
|
||||
- [x] File transfers
|
||||
- [x] Audio calls
|
||||
- [x] Plugins support
|
||||
- [x] Chat history
|
||||
- [x] Emoticons
|
||||
- [x] Stickers
|
||||
- [x] Screenshots
|
||||
- [x] Name lookups (toxme.io support)
|
||||
- [x] Save file encryption
|
||||
- [x] Profile import and export
|
||||
- [x] Faux offline messaging
|
||||
- [x] Faux offline file transfers
|
||||
- [x] Inline images
|
||||
- [x] Message splitting
|
||||
- [x] Proxy support
|
||||
- [x] Avatars
|
||||
- [x] Multiprofile
|
||||
- [x] Multilingual
|
||||
- [x] Sound notifications
|
||||
- [x] Contact aliases
|
||||
- [x] Contact blocking
|
||||
- [x] Typing notifications
|
||||
- [x] Changing nospam
|
||||
- [x] File resuming
|
||||
- [x] Read receipts
|
||||
- [ ] Video calls
|
||||
- [ ] Desktop sharing
|
||||
- [ ] Group chats
|
||||
- 1v1 messages
|
||||
- File transfers
|
||||
- Audio calls
|
||||
- Video calls
|
||||
- Plugins support
|
||||
- Desktop sharing
|
||||
- Chat history
|
||||
- Emoticons
|
||||
- Stickers
|
||||
- Screenshots
|
||||
- Name lookups (toxme.io support)
|
||||
- Save file encryption
|
||||
- Profile import and export
|
||||
- Faux offline messaging
|
||||
- Faux offline file transfers
|
||||
- Inline images
|
||||
- Message splitting
|
||||
- Proxy support
|
||||
- Avatars
|
||||
- Multiprofile
|
||||
- Multilingual
|
||||
- Sound notifications
|
||||
- Contact aliases
|
||||
- Contact blocking
|
||||
- Typing notifications
|
||||
- Changing nospam
|
||||
- File resuming
|
||||
- Read receipts
|
||||
|
||||
### Downloads
|
||||
[Releases](https://github.com/toxygen-project/toxygen/releases)
|
||||
|
@ -1,20 +1,22 @@
|
||||
#Issues
|
||||
# Issues
|
||||
|
||||
Help us find all bugs in Toxygen! Please provide following info:
|
||||
|
||||
- OS
|
||||
- Toxygen version
|
||||
- Toxygen executable info - .py or precompiled binary, how was it installed in system
|
||||
- Toxygen executable info - python executable (.py), precompiled binary, from package etc.
|
||||
- Steps to reproduce the bug
|
||||
|
||||
Want to see new feature in Toxygen? [Ask for it!](https://github.com/toxygen-project/toxygen/issues)
|
||||
|
||||
#Pull requests
|
||||
# Pull requests
|
||||
|
||||
Developer? Feel free to open pull request. Our dev team is small so we glad to get help.
|
||||
Don't know what to do? Improve UI, fix [issues](https://github.com/toxygen-project/toxygen/issues) or implement features from our TODO list.
|
||||
You can find our TODO's in code, issues list and [here](/README.md). Also you can implement [plugins](/docs/plugins.md) for Toxygen.
|
||||
|
||||
#Translations
|
||||
Note that we have a lot of branches for different purposes. Master branch is for stable versions (releases) only, so I recommend to open PR's to develop branch. Development of next Toxygen version usually goes there. Other branches used for implementing different tasks such as file transfers improvements or audio calls implementation etc.
|
||||
|
||||
Help us translate Toxygen! Translation can be created using pyside-lupdate (``pyside-lupdate toxygen.pro``) and QT Linguist.
|
||||
# Translations
|
||||
|
||||
Help us translate Toxygen! Translation can be created using pylupdate (``pylupdate5 toxygen.pro``) and QT Linguist.
|
||||
|
@ -1,13 +1,13 @@
|
||||
# How to install Toxygen
|
||||
|
||||
## Use precompiled binary:
|
||||
## Use precompiled binary (recommended for users):
|
||||
[Check our releases page](https://github.com/toxygen-project/toxygen/releases)
|
||||
|
||||
## Using pip3
|
||||
|
||||
### Windows
|
||||
|
||||
``pip3.4 install toxygen``
|
||||
``pip install toxygen``
|
||||
|
||||
Run app using ``toxygen`` command.
|
||||
|
||||
@ -16,19 +16,11 @@ Run app using ``toxygen`` command.
|
||||
1. Install [toxcore](https://github.com/irungentoo/toxcore/blob/master/INSTALL.md) with toxav support in your system (install in /usr/lib/)
|
||||
2. Install PortAudio:
|
||||
``sudo apt-get install portaudio19-dev``
|
||||
3. Install PySide: ``sudo apt-get install python3-pyside``
|
||||
4. Install toxygen:
|
||||
``sudo pip3.4 install toxygen``
|
||||
5. Run toxygen using ``toxygen`` command.
|
||||
|
||||
### OS X
|
||||
|
||||
1. Install [toxcore](https://github.com/irungentoo/toxcore/blob/master/INSTALL.md) with toxav support in your system
|
||||
2. Install PortAudio:
|
||||
``brew install portaudio``
|
||||
3. Install toxygen:
|
||||
``pip3.4 install toxygen``
|
||||
4. Run toxygen using ``toxygen`` command.
|
||||
3. For 32-bit Linux install PyQt5: ``sudo apt-get install python3-pyqt5``
|
||||
4. Install [OpenCV](http://docs.opencv.org/trunk/d7/d9f/tutorial_linux_install.html) or via ``sudo apt-get install python3-opencv``
|
||||
5. Install toxygen:
|
||||
``sudo pip3 install toxygen``
|
||||
6. Run toxygen using ``toxygen`` command.
|
||||
|
||||
## Packages
|
||||
|
||||
@ -40,15 +32,19 @@ Debian/Ubuntu: [tox.chat](https://tox.chat/download.html#gnulinux)
|
||||
|
||||
### Windows
|
||||
|
||||
1. [Download and install latest Python 3.4](https://www.python.org/downloads/windows/)
|
||||
2. [Install PySide](https://pypi.python.org/pypi/PySide/1.2.4#installing-pyside-on-a-windows-system) (recommended) or [PyQt4](https://riverbankcomputing.com/software/pyqt/download)
|
||||
3. Install PyAudio: ``pip3.4 install pyaudio``
|
||||
4. [Download toxygen](https://github.com/xveduk/toxygen/archive/master.zip)
|
||||
5. Unpack archive
|
||||
6. Download latest libtox.dll build, download latest libsodium.a build, put it into \src\libs\
|
||||
7. Run \toxygen\main.py.
|
||||
Note: 32-bit Python isn't supported due to bug with videocalls. It is strictly recommended to use 64-bit Python.
|
||||
|
||||
Optional: install toxygen using setup.py: ``python3.4 setup.py install``
|
||||
1. [Download and install latest Python 3 64-bit](https://www.python.org/downloads/windows/)
|
||||
2. Install PyQt5: ``pip install pyqt5``
|
||||
3. Install PyAudio: ``pip install pyaudio``
|
||||
4. Install numpy: ``pip install numpy``
|
||||
5. Install OpenCV: ``pip install opencv-python``
|
||||
6. [Download toxygen](https://github.com/toxygen-project/toxygen/archive/master.zip)
|
||||
7. Unpack archive
|
||||
8. Download latest libtox.dll build, download latest libsodium.a build, put it into \toxygen\libs\
|
||||
9. Run \toxygen\main.py.
|
||||
|
||||
Optional: install toxygen using setup.py: ``python setup.py install``
|
||||
|
||||
[libtox.dll for 32-bit Python](https://build.tox.chat/view/libtoxcore/job/libtoxcore_build_windows_x86_shared_release/lastSuccessfulBuild/artifact/libtoxcore_build_windows_x86_shared_release.zip)
|
||||
|
||||
@ -62,27 +58,15 @@ Optional: install toxygen using setup.py: ``python3.4 setup.py install``
|
||||
|
||||
1. Install latest Python3:
|
||||
``sudo apt-get install python3``
|
||||
2. Install PySide: ``sudo apt-get install python3-pyside`` or install [PyQt4](https://riverbankcomputing.com/software/pyqt/download) (``sudo apt-get install python3-pyqt4``).
|
||||
2. Install PyQt5: ``sudo apt-get install python3-pyqt5`` or ``sudo pip3 install pyqt5``
|
||||
3. Install [toxcore](https://github.com/irungentoo/toxcore/blob/master/INSTALL.md) with toxav support in your system (install in /usr/lib/)
|
||||
4. Install PyAudio:
|
||||
``sudo apt-get install portaudio19-dev`` and ``sudo apt-get install python3-pyaudio`` (or ``pip3 install pyaudio``)
|
||||
5. [Download toxygen](https://github.com/xveduk/toxygen/archive/master.zip)
|
||||
6. Unpack archive
|
||||
7. Run app:
|
||||
``python3.4 main.py``
|
||||
|
||||
Optional: install toxygen using setup.py: ``python3.4 setup.py install``
|
||||
|
||||
### OS X
|
||||
|
||||
1. [Download and install latest Python 3.4](https://www.python.org/downloads/mac-osx/)
|
||||
2. [Install PySide](https://pypi.python.org/pypi/PySide/1.2.4#installing-pyside-on-a-mac-os-x-system) (recommended) or [PyQt4](https://riverbankcomputing.com/software/pyqt/download)
|
||||
3. Install PortAudio:
|
||||
``brew install portaudio``
|
||||
4. Install PyAudio: ``pip3 install pyaudio``
|
||||
5. Install [toxcore](https://github.com/irungentoo/toxcore/blob/master/INSTALL.md) with toxav support in your system
|
||||
6. [Download toxygen](https://github.com/xveduk/toxygen/archive/master.zip)
|
||||
7. Unpack archive
|
||||
8. Run \toxygen\main.py.
|
||||
``sudo apt-get install portaudio19-dev`` and ``sudo apt-get install python3-pyaudio`` (or ``sudo pip3 install pyaudio``)
|
||||
5. Install NumPy: ``sudo pip3 install numpy``
|
||||
6. Install [OpenCV](http://docs.opencv.org/trunk/d7/d9f/tutorial_linux_install.html) or via ``sudo apt-get install python3-opencv``
|
||||
7. [Download toxygen](https://github.com/toxygen-project/toxygen/archive/master.zip)
|
||||
8. Unpack archive
|
||||
9. Run app:
|
||||
``python3 main.py``
|
||||
|
||||
Optional: install toxygen using setup.py: ``python3 setup.py install``
|
||||
|
BIN
docs/os.png
Before Width: | Height: | Size: 28 KiB |
@ -1,6 +1,6 @@
|
||||
# Plugins API
|
||||
|
||||
In Toxygen plugin is single python (supported Python 3.0 - 3.4) module (.py file) and directory with data associated with it.
|
||||
In Toxygen plugin is single python (supported Python 3.4 - 3.6) module (.py file) and directory with data associated with it.
|
||||
Every module must contain one class derived from PluginSuperClass defined in [plugin_super_class.py](/src/plugins/plugin_super_class.py). Instance of this class will be created by PluginLoader class (defined in [plugin_support.py](/src/plugin_support.py) ). This class can enable/disable plugins and send data to it.
|
||||
|
||||
Every plugin has its own full name and unique short name (1-5 symbols). Main app can get it using special methods.
|
||||
@ -45,7 +45,7 @@ Import statement will not work in case you import module that wasn't previously
|
||||
|
||||
About GUI:
|
||||
|
||||
It's strictly recommended to support both PySide and PyQt4 in GUI. Plugin can have no GUI at all.
|
||||
GUI is available via PyQt5. Plugin can have no GUI at all.
|
||||
|
||||
Exceptions:
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Plugins
|
||||
|
||||
Toxygen is the first [Tox](https://tox.chat/) client with plugins support. Plugin is Python 3.4 module (.py file) and directory with plugin's data which provide some additional functionality.
|
||||
Toxygen is the first [Tox](https://tox.chat/) client with plugins support. Plugin is Python 3.4 - 3.6 module (.py file) and directory with plugin's data which provide some additional functionality.
|
||||
|
||||
# How to write plugin
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
#Smileys
|
||||
# Smileys
|
||||
|
||||
Toxygen support smileys. Smiley is small picture which replaces some symbol or combination of symbols. If you want to create your own smiley pack, create directory in src/smileys/. This directory must contain images with smileys and config.json. Example of config.json:
|
||||
|
||||
@ -6,7 +6,7 @@ Toxygen support smileys. Smiley is small picture which replaces some symbol or c
|
||||
|
||||
Animated smileys (.gif) are supported too.
|
||||
|
||||
#Stickers
|
||||
# Stickers
|
||||
|
||||
Sticker is inline image. If you want to create your own smiley pack, create directory in src/stickers/ and place your stickers there.
|
||||
|
||||
|
27
setup.py
@ -8,15 +8,23 @@ import sys
|
||||
|
||||
version = program_version + '.0'
|
||||
|
||||
MODULES = []
|
||||
|
||||
if system() in ('Windows', 'Darwin'):
|
||||
MODULES = ['PyAudio', 'PyQt5']
|
||||
if system() == 'Windows':
|
||||
MODULES = ['PyQt5', 'PyAudio', 'numpy', 'opencv-python']
|
||||
else:
|
||||
MODULES = []
|
||||
try:
|
||||
import pyaudio
|
||||
except ImportError:
|
||||
MODULES = ['PyAudio']
|
||||
MODULES.append('PyAudio')
|
||||
try:
|
||||
import PyQt5
|
||||
except ImportError:
|
||||
MODULES.append('PyQt5')
|
||||
try:
|
||||
import numpy
|
||||
except ImportError:
|
||||
MODULES.append('numpy')
|
||||
|
||||
|
||||
class InstallScript(install):
|
||||
@ -25,9 +33,7 @@ class InstallScript(install):
|
||||
def run(self):
|
||||
install.run(self)
|
||||
try:
|
||||
if system() == 'Windows':
|
||||
call(["toxygen", "--configure"])
|
||||
else:
|
||||
if system() != 'Windows':
|
||||
call(["toxygen", "--clean"])
|
||||
except:
|
||||
try:
|
||||
@ -37,9 +43,7 @@ class InstallScript(install):
|
||||
if path[-1] not in ('/', '\\'):
|
||||
path += '/'
|
||||
path += 'bin/toxygen'
|
||||
if system() == 'Windows':
|
||||
call([path, "--configure"])
|
||||
else:
|
||||
if system() != 'Windows':
|
||||
call([path, "--clean"])
|
||||
except:
|
||||
pass
|
||||
@ -66,5 +70,4 @@ setup(name='Toxygen',
|
||||
},
|
||||
cmdclass={
|
||||
'install': InstallScript,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
@ -19,6 +19,7 @@ class IncomingCallWidget(widgets.CenteredWidget):
|
||||
self.avatar_label.setScaledContents(False)
|
||||
self.name = widgets.DataLabel(self)
|
||||
self.name.setGeometry(QtCore.QRect(90, 20, 300, 25))
|
||||
self._friend_number = friend_number
|
||||
font = QtGui.QFont()
|
||||
font.setFamily(settings.Settings.get_instance()['font'])
|
||||
font.setPointSize(16)
|
||||
@ -51,10 +52,10 @@ class IncomingCallWidget(widgets.CenteredWidget):
|
||||
self.setWindowTitle(text)
|
||||
self.name.setText(name)
|
||||
self.call_type.setText(text)
|
||||
pr = profile.Profile.get_instance()
|
||||
self.accept_audio.clicked.connect(lambda: pr.accept_call(friend_number, True, False) or self.stop())
|
||||
# self.accept_video.clicked.connect(lambda: pr.start_call(friend_number, True, True))
|
||||
self.decline.clicked.connect(lambda: pr.stop_call(friend_number, False) or self.stop())
|
||||
self._processing = False
|
||||
self.accept_audio.clicked.connect(self.accept_call_with_audio)
|
||||
self.accept_video.clicked.connect(self.accept_call_with_video)
|
||||
self.decline.clicked.connect(self.decline_call)
|
||||
|
||||
class SoundPlay(QtCore.QThread):
|
||||
|
||||
@ -105,31 +106,29 @@ class IncomingCallWidget(widgets.CenteredWidget):
|
||||
self.thread.wait()
|
||||
self.close()
|
||||
|
||||
def accept_call_with_audio(self):
|
||||
if self._processing:
|
||||
return
|
||||
self._processing = True
|
||||
pr = profile.Profile.get_instance()
|
||||
pr.accept_call(self._friend_number, True, False)
|
||||
self.stop()
|
||||
|
||||
def accept_call_with_video(self):
|
||||
if self._processing:
|
||||
return
|
||||
self._processing = True
|
||||
pr = profile.Profile.get_instance()
|
||||
pr.accept_call(self._friend_number, True, True)
|
||||
self.stop()
|
||||
|
||||
def decline_call(self):
|
||||
if self._processing:
|
||||
return
|
||||
self._processing = True
|
||||
pr = profile.Profile.get_instance()
|
||||
pr.stop_call(self._friend_number, False)
|
||||
self.stop()
|
||||
|
||||
def set_pixmap(self, pixmap):
|
||||
self.avatar_label.setPixmap(pixmap)
|
||||
|
||||
|
||||
class AudioMessageRecorder(widgets.CenteredWidget):
|
||||
|
||||
def __init__(self, friend_number, name):
|
||||
super(AudioMessageRecorder, self).__init__()
|
||||
self.label = QtWidgets.QLabel(self)
|
||||
self.label.setGeometry(QtCore.QRect(10, 20, 250, 20))
|
||||
text = QtWidgets.QApplication.translate("MenuWindow", "Send audio message to friend {}")
|
||||
self.label.setText(text.format(name))
|
||||
self.record = QtWidgets.QPushButton(self)
|
||||
self.record.setGeometry(QtCore.QRect(20, 100, 150, 150))
|
||||
|
||||
self.record.setText(QtWidgets.QApplication.translate("MenuWindow", "Start recording"))
|
||||
self.record.clicked.connect(self.start_or_stop_recording)
|
||||
self.recording = False
|
||||
self.friend_num = friend_number
|
||||
|
||||
def start_or_stop_recording(self):
|
||||
if not self.recording:
|
||||
self.recording = True
|
||||
self.record.setText(QtWidgets.QApplication.translate("MenuWindow", "Stop recording"))
|
||||
else:
|
||||
self.close()
|
||||
|
||||
|
||||
|
@ -9,7 +9,8 @@ from plugin_support import PluginLoader
|
||||
import queue
|
||||
import threading
|
||||
import util
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Threads
|
||||
@ -312,11 +313,67 @@ def callback_audio(toxav, friend_number, samples, audio_samples_per_channel, aud
|
||||
"""
|
||||
New audio chunk
|
||||
"""
|
||||
Profile.get_instance().call.chunk(
|
||||
Profile.get_instance().call.audio_chunk(
|
||||
bytes(samples[:audio_samples_per_channel * 2 * audio_channels_count]),
|
||||
audio_channels_count,
|
||||
rate)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Callbacks - video
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, ustride, vstride, user_data):
|
||||
"""
|
||||
Creates yuv frame from y, u, v and shows it using OpenCV
|
||||
For yuv => bgr we need this YUV420 frame:
|
||||
|
||||
width
|
||||
-------------------------
|
||||
| |
|
||||
| Y | height
|
||||
| |
|
||||
-------------------------
|
||||
| | |
|
||||
| U even | U odd | height // 4
|
||||
| | |
|
||||
-------------------------
|
||||
| | |
|
||||
| V even | V odd | height // 4
|
||||
| | |
|
||||
-------------------------
|
||||
|
||||
width // 2 width // 2
|
||||
|
||||
It can be created from initial y, u, v using slices
|
||||
For more info see callback_video_receive_frame docs
|
||||
"""
|
||||
try:
|
||||
y_size = abs(max(width, abs(ystride)))
|
||||
u_size = abs(max(width // 2, abs(ustride)))
|
||||
v_size = abs(max(width // 2, abs(vstride)))
|
||||
|
||||
y = np.asarray(y[:y_size * height], dtype=np.uint8).reshape(height, y_size)
|
||||
u = np.asarray(u[:u_size * height // 2], dtype=np.uint8).reshape(height // 2, u_size)
|
||||
v = np.asarray(v[:v_size * height // 2], dtype=np.uint8).reshape(height // 2, v_size)
|
||||
|
||||
width -= width % 4
|
||||
height -= height % 4
|
||||
|
||||
frame = np.zeros((int(height * 1.5), width), dtype=np.uint8)
|
||||
|
||||
frame[:height, :] = y[:height, :width]
|
||||
frame[height:height * 5 // 4, :width // 2] = u[:height // 2:2, :width // 2]
|
||||
frame[height:height * 5 // 4, width // 2:] = u[1:height // 2:2, :width // 2]
|
||||
|
||||
frame[height * 5 // 4:, :width // 2] = v[:height // 2:2, :width // 2]
|
||||
frame[height * 5 // 4:, width // 2:] = v[1:height // 2:2, :width // 2]
|
||||
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
|
||||
|
||||
invoke_in_main_thread(cv2.imshow, str(friend_number), frame)
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Callbacks - initialization
|
||||
@ -350,7 +407,7 @@ def init_callbacks(tox, window, tray):
|
||||
toxav.callback_call_state(call_state, 0)
|
||||
toxav.callback_call(call, 0)
|
||||
toxav.callback_audio_receive_frame(callback_audio, 0)
|
||||
toxav.callback_video_receive_frame(video_receive_frame, 0)
|
||||
|
||||
tox.callback_friend_lossless_packet(lossless_packet, 0)
|
||||
tox.callback_friend_lossy_packet(lossy_packet, 0)
|
||||
|
||||
|
272
toxygen/calls.py
@ -3,14 +3,69 @@ import time
|
||||
import threading
|
||||
import settings
|
||||
from toxav_enums import *
|
||||
# TODO: play sound until outgoing call will be started or cancelled and add timeout
|
||||
# TODO: add widget for call
|
||||
import cv2
|
||||
import itertools
|
||||
import numpy as np
|
||||
import screen_sharing
|
||||
# TODO: play sound until outgoing call will be started or cancelled
|
||||
|
||||
CALL_TYPE = {
|
||||
'NONE': 0,
|
||||
'AUDIO': 1,
|
||||
'VIDEO': 2
|
||||
}
|
||||
|
||||
class Call:
|
||||
|
||||
def __init__(self, out_audio, out_video, in_audio=False, in_video=False):
|
||||
self._in_audio = in_audio
|
||||
self._in_video = in_video
|
||||
self._out_audio = out_audio
|
||||
self._out_video = out_video
|
||||
self._is_active = False
|
||||
|
||||
def get_is_active(self):
|
||||
return self._is_active
|
||||
|
||||
def set_is_active(self, value):
|
||||
self._is_active = value
|
||||
|
||||
is_active = property(get_is_active, set_is_active)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Audio
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def get_in_audio(self):
|
||||
return self._in_audio
|
||||
|
||||
def set_in_audio(self, value):
|
||||
self._in_audio = value
|
||||
|
||||
in_audio = property(get_in_audio, set_in_audio)
|
||||
|
||||
def get_out_audio(self):
|
||||
return self._out_audio
|
||||
|
||||
def set_out_audio(self, value):
|
||||
self._out_audio = value
|
||||
|
||||
out_audio = property(get_out_audio, set_out_audio)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Video
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def get_in_video(self):
|
||||
return self._in_video
|
||||
|
||||
def set_in_video(self, value):
|
||||
self._in_video = value
|
||||
|
||||
in_video = property(get_in_video, set_in_video)
|
||||
|
||||
def get_out_video(self):
|
||||
return self._out_video
|
||||
|
||||
def set_out_video(self, value):
|
||||
self._in_video = value
|
||||
|
||||
out_video = property(get_out_video, set_out_video)
|
||||
|
||||
|
||||
class AV:
|
||||
@ -19,7 +74,7 @@ class AV:
|
||||
self._toxav = toxav
|
||||
self._running = True
|
||||
|
||||
self._calls = {} # dict: key - friend number, value - call type
|
||||
self._calls = {} # dict: key - friend number, value - Call instance
|
||||
|
||||
self._audio = None
|
||||
self._audio_stream = None
|
||||
@ -32,27 +87,75 @@ class AV:
|
||||
self._audio_duration = 60
|
||||
self._audio_sample_count = self._audio_rate * self._audio_channels * self._audio_duration // 1000
|
||||
|
||||
def __contains__(self, friend_number):
|
||||
return friend_number in self._calls
|
||||
self._video = None
|
||||
self._video_thread = None
|
||||
self._video_running = False
|
||||
|
||||
def __call__(self, friend_number, audio, video):
|
||||
"""Call friend with specified number"""
|
||||
self._toxav.call(friend_number, 32 if audio else 0, 5000 if video else 0)
|
||||
self._calls[friend_number] = CALL_TYPE['AUDIO']
|
||||
self.start_audio_thread()
|
||||
|
||||
def finish_call(self, friend_number, by_friend=False):
|
||||
|
||||
if not by_friend:
|
||||
self._toxav.call_control(friend_number, TOXAV_CALL_CONTROL['CANCEL'])
|
||||
if friend_number in self._calls:
|
||||
del self._calls[friend_number]
|
||||
if not len(self._calls):
|
||||
self.stop_audio_thread()
|
||||
self._video_width = 640
|
||||
self._video_height = 480
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
self.stop_audio_thread()
|
||||
self.stop_video_thread()
|
||||
|
||||
def __contains__(self, friend_number):
|
||||
return friend_number in self._calls
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Calls
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def __call__(self, friend_number, audio, video):
|
||||
"""Call friend with specified number"""
|
||||
self._toxav.call(friend_number, 32 if audio else 0, 5000 if video else 0)
|
||||
self._calls[friend_number] = Call(audio, video)
|
||||
threading.Timer(30.0, lambda: self.finish_not_started_call(friend_number)).start()
|
||||
|
||||
def accept_call(self, friend_number, audio_enabled, video_enabled):
|
||||
if self._running:
|
||||
self._calls[friend_number] = Call(audio_enabled, video_enabled)
|
||||
self._toxav.answer(friend_number, 32 if audio_enabled else 0, 5000 if video_enabled else 0)
|
||||
if audio_enabled:
|
||||
self.start_audio_thread()
|
||||
if video_enabled:
|
||||
self.start_video_thread()
|
||||
|
||||
def finish_call(self, friend_number, by_friend=False):
|
||||
if not by_friend:
|
||||
self._toxav.call_control(friend_number, TOXAV_CALL_CONTROL['CANCEL'])
|
||||
if friend_number in self._calls:
|
||||
del self._calls[friend_number]
|
||||
if not len(list(filter(lambda c: c.out_audio, self._calls))):
|
||||
self.stop_audio_thread()
|
||||
if not len(list(filter(lambda c: c.out_video, self._calls))):
|
||||
self.stop_video_thread()
|
||||
|
||||
def finish_not_started_call(self, friend_number):
|
||||
if friend_number in self:
|
||||
call = self._calls[friend_number]
|
||||
if not call.is_active:
|
||||
self.finish_call(friend_number)
|
||||
|
||||
def toxav_call_state_cb(self, friend_number, state):
|
||||
"""
|
||||
New call state
|
||||
"""
|
||||
call = self._calls[friend_number]
|
||||
call.is_active = True
|
||||
|
||||
call.in_audio = state | TOXAV_FRIEND_CALL_STATE['SENDING_A']
|
||||
call.in_video = state | TOXAV_FRIEND_CALL_STATE['SENDING_V']
|
||||
|
||||
if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_A'] and call.out_audio:
|
||||
self.start_audio_thread()
|
||||
|
||||
if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_V'] and call.out_video:
|
||||
self.start_video_thread()
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Threads
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def start_audio_thread(self):
|
||||
"""
|
||||
@ -92,7 +195,41 @@ class AV:
|
||||
self._out_stream.close()
|
||||
self._out_stream = None
|
||||
|
||||
def chunk(self, samples, channels_count, rate):
|
||||
def start_video_thread(self):
|
||||
if self._video_thread is not None:
|
||||
return
|
||||
|
||||
self._video_running = True
|
||||
s = settings.Settings.get_instance()
|
||||
self._video_width = s.video['width']
|
||||
self._video_height = s.video['height']
|
||||
|
||||
if s.video['device'] == -1:
|
||||
self._video = screen_sharing.DesktopGrabber(s.video['x'], s.video['y'],
|
||||
s.video['width'], s.video['height'])
|
||||
else:
|
||||
self._video = cv2.VideoCapture(s.video['device'])
|
||||
self._video.set(cv2.CAP_PROP_FPS, 25)
|
||||
self._video.set(cv2.CAP_PROP_FRAME_WIDTH, self._video_width)
|
||||
self._video.set(cv2.CAP_PROP_FRAME_HEIGHT, self._video_height)
|
||||
|
||||
self._video_thread = threading.Thread(target=self.send_video)
|
||||
self._video_thread.start()
|
||||
|
||||
def stop_video_thread(self):
|
||||
if self._video_thread is None:
|
||||
return
|
||||
|
||||
self._video_running = False
|
||||
self._video_thread.join()
|
||||
self._video_thread = None
|
||||
self._video = None
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Incoming chunks
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def audio_chunk(self, samples, channels_count, rate):
|
||||
"""
|
||||
Incoming chunk
|
||||
"""
|
||||
@ -105,6 +242,10 @@ class AV:
|
||||
output=True)
|
||||
self._out_stream.write(samples)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# AV sending
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def send_audio(self):
|
||||
"""
|
||||
This method sends audio to friends
|
||||
@ -114,10 +255,10 @@ class AV:
|
||||
try:
|
||||
pcm = self._audio_stream.read(self._audio_sample_count)
|
||||
if pcm:
|
||||
for friend in self._calls:
|
||||
if self._calls[friend] & 1:
|
||||
for friend_num in self._calls:
|
||||
if self._calls[friend_num].out_audio:
|
||||
try:
|
||||
self._toxav.audio_send_frame(friend, pcm, self._audio_sample_count,
|
||||
self._toxav.audio_send_frame(friend_num, pcm, self._audio_sample_count,
|
||||
self._audio_channels, self._audio_rate)
|
||||
except:
|
||||
pass
|
||||
@ -126,19 +267,70 @@ class AV:
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
def accept_call(self, friend_number, audio_enabled, video_enabled):
|
||||
|
||||
if self._running:
|
||||
self._calls[friend_number] = int(video_enabled) * 2 + int(audio_enabled)
|
||||
self._toxav.answer(friend_number, 32 if audio_enabled else 0, 5000 if video_enabled else 0)
|
||||
self.start_audio_thread()
|
||||
|
||||
def toxav_call_state_cb(self, friend_number, state):
|
||||
def send_video(self):
|
||||
"""
|
||||
New call state
|
||||
This method sends video to friends
|
||||
"""
|
||||
if self._running:
|
||||
while self._video_running:
|
||||
try:
|
||||
result, frame = self._video.read()
|
||||
if result:
|
||||
height, width, channels = frame.shape
|
||||
for friend_num in self._calls:
|
||||
if self._calls[friend_num].out_video:
|
||||
try:
|
||||
y, u, v = self.convert_bgr_to_yuv(frame)
|
||||
self._toxav.video_send_frame(friend_num, width, height, y, u, v)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
if state & TOXAV_FRIEND_CALL_STATE['ACCEPTING_A']:
|
||||
self._calls[friend_number] |= 1
|
||||
time.sleep(0.01)
|
||||
|
||||
def convert_bgr_to_yuv(self, frame):
|
||||
"""
|
||||
:param frame: input bgr frame
|
||||
:return y, u, v: y, u, v values of frame
|
||||
|
||||
How this function works:
|
||||
OpenCV creates YUV420 frame from BGR
|
||||
This frame has following structure and size:
|
||||
width, height - dim of input frame
|
||||
width, height * 1.5 - dim of output frame
|
||||
|
||||
width
|
||||
-------------------------
|
||||
| |
|
||||
| Y | height
|
||||
| |
|
||||
-------------------------
|
||||
| | |
|
||||
| U even | U odd | height // 4
|
||||
| | |
|
||||
-------------------------
|
||||
| | |
|
||||
| V even | V odd | height // 4
|
||||
| | |
|
||||
-------------------------
|
||||
|
||||
width // 2 width // 2
|
||||
|
||||
Y, U, V can be extracted using slices and joined in one list using itertools.chain.from_iterable()
|
||||
Function returns bytes(y), bytes(u), bytes(v), because it is required for ctypes
|
||||
"""
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV_I420)
|
||||
|
||||
y = frame[:self._video_height, :]
|
||||
y = list(itertools.chain.from_iterable(y))
|
||||
|
||||
u = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int)
|
||||
u[::2, :] = frame[self._video_height:self._video_height * 5 // 4, :self._video_width // 2]
|
||||
u[1::2, :] = frame[self._video_height:self._video_height * 5 // 4, self._video_width // 2:]
|
||||
u = list(itertools.chain.from_iterable(u))
|
||||
v = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int)
|
||||
v[::2, :] = frame[self._video_height * 5 // 4:, :self._video_width // 2]
|
||||
v[1::2, :] = frame[self._video_height * 5 // 4:, self._video_width // 2:]
|
||||
v = list(itertools.chain.from_iterable(v))
|
||||
|
||||
return bytes(y), bytes(u), bytes(v)
|
||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.0 KiB |
@ -533,16 +533,14 @@ class InlineImageItem(QtWidgets.QScrollArea):
|
||||
self._elem.setSizeHint(QtCore.QSize(self.width(), self.height()))
|
||||
elif event.button() == QtCore.Qt.RightButton: # save inline
|
||||
directory = QtWidgets.QFileDialog.getExistingDirectory(self,
|
||||
QtWidgets.QApplication.translate("MainWindow",
|
||||
'Choose folder'),
|
||||
curr_directory(),
|
||||
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
|
||||
QtWidgets.QApplication.translate("MainWindow",
|
||||
'Choose folder'),
|
||||
curr_directory(),
|
||||
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
|
||||
if directory:
|
||||
fl = QtCore.QFile(directory + '/toxygen_inline_' + curr_time().replace(':', '_') + '.png')
|
||||
self._pixmap.save(fl, 'PNG')
|
||||
|
||||
return False
|
||||
|
||||
def mark_as_sent(self):
|
||||
return False
|
||||
|
||||
|
@ -54,10 +54,9 @@ class Toxygen:
|
||||
if platform.system() == 'Linux':
|
||||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
|
||||
|
||||
# application color scheme
|
||||
with open(curr_directory() + '/styles/style.qss') as fl:
|
||||
dark_style = fl.read()
|
||||
app.setStyleSheet(dark_style)
|
||||
with open(curr_directory() + '/styles/dark_style.qss') as fl:
|
||||
style = fl.read()
|
||||
app.setStyleSheet(style)
|
||||
|
||||
encrypt_save = toxes.ToxES()
|
||||
|
||||
@ -101,17 +100,17 @@ class Toxygen:
|
||||
msgBox.setWindowTitle(
|
||||
QtWidgets.QApplication.translate("MainWindow", "Error"))
|
||||
text = (QtWidgets.QApplication.translate("MainWindow",
|
||||
'Profile with this name already exists'))
|
||||
'Profile with this name already exists'))
|
||||
msgBox.setText(text)
|
||||
msgBox.exec_()
|
||||
return
|
||||
self.tox = profile.tox_factory()
|
||||
self.tox.self_set_name(bytes(_login.name, 'utf-8') if _login.name else b'Toxygen User')
|
||||
self.tox.self_set_status_message(b'Toxing on Toxygen')
|
||||
self.tox.self_set_status_message(b'Toxing on T03')
|
||||
reply = QtWidgets.QMessageBox.question(None,
|
||||
'Profile {}'.format(name),
|
||||
QtWidgets.QApplication.translate("login",
|
||||
'Do you want to set profile password?'),
|
||||
'Profile {}'.format(name),
|
||||
QtWidgets.QApplication.translate("login",
|
||||
'Do you want to set profile password?'),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
@ -120,10 +119,10 @@ class Toxygen:
|
||||
self.app.lastWindowClosed.connect(self.app.quit)
|
||||
self.app.exec_()
|
||||
reply = QtWidgets.QMessageBox.question(None,
|
||||
'Profile {}'.format(name),
|
||||
QtWidgets.QApplication.translate("login",
|
||||
'Do you want to save profile in default folder? If no, profile will be saved in program folder'),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
'Profile {}'.format(name),
|
||||
QtWidgets.QApplication.translate("login",
|
||||
'Do you want to save profile in default folder? If no, profile will be saved in program folder'),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
path = Settings.get_default_path()
|
||||
@ -136,7 +135,7 @@ class Toxygen:
|
||||
log('Profile creation exception: ' + str(ex))
|
||||
msgBox = QtWidgets.QMessageBox()
|
||||
msgBox.setText(QtWidgets.QApplication.translate("login",
|
||||
'Profile saving error! Does Toxygen have permission to write to this directory?'))
|
||||
'Profile saving error! Does Toxygen have permission to write to this directory?'))
|
||||
msgBox.exec_()
|
||||
return
|
||||
path = Settings.get_default_path()
|
||||
@ -163,8 +162,8 @@ class Toxygen:
|
||||
|
||||
if Settings.is_active_profile(path, name): # profile is in use
|
||||
reply = QtWidgets.QMessageBox.question(None,
|
||||
'Profile {}'.format(name),
|
||||
QtWidgets.QApplication.translate("login", 'Other instance of Toxygen uses this profile or profile was not properly closed. Continue?'),
|
||||
'Profile {}'.format(name),
|
||||
QtWidgets.QApplication.translate("login", 'Other instance of Toxygen uses this profile or profile was not properly closed. Continue?'),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply != QtWidgets.QMessageBox.Yes:
|
||||
@ -172,6 +171,13 @@ class Toxygen:
|
||||
else:
|
||||
settings.set_active_profile()
|
||||
|
||||
# application color scheme
|
||||
for theme in settings.built_in_themes().keys():
|
||||
if settings['theme'] == theme:
|
||||
with open(curr_directory() + settings.built_in_themes()[theme]) as fl:
|
||||
style = fl.read()
|
||||
app.setStyleSheet(style)
|
||||
|
||||
lang = Settings.supported_languages()[settings['language']]
|
||||
translator = QtCore.QTranslator()
|
||||
translator.load(curr_directory() + '/translations/' + lang)
|
||||
@ -277,9 +283,9 @@ class Toxygen:
|
||||
updating = True
|
||||
else:
|
||||
reply = QtWidgets.QMessageBox.question(None,
|
||||
'Toxygen',
|
||||
QtWidgets.QApplication.translate("login",
|
||||
'Update for Toxygen was found. Download and install it?'),
|
||||
'Toxygen',
|
||||
QtWidgets.QApplication.translate("login",
|
||||
'Update for Toxygen was found. Download and install it?'),
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
@ -448,27 +454,6 @@ def clean():
|
||||
remove(d)
|
||||
|
||||
|
||||
def configure():
|
||||
"""Removes unused libs"""
|
||||
d = curr_directory() + '/libs/'
|
||||
is_64bits = is_64_bit()
|
||||
if not is_64bits:
|
||||
if os.path.exists(d + 'libtox64.dll'):
|
||||
os.remove(d + 'libtox64.dll')
|
||||
if os.path.exists(d + 'libsodium64.a'):
|
||||
os.remove(d + 'libsodium64.a')
|
||||
else:
|
||||
if os.path.exists(d + 'libtox.dll'):
|
||||
os.remove(d + 'libtox.dll')
|
||||
if os.path.exists(d + 'libsodium.a'):
|
||||
os.remove(d + 'libsodium.a')
|
||||
try:
|
||||
os.rename(d + 'libtox64.dll', d + 'libtox.dll')
|
||||
os.rename(d + 'libsodium64.a', d + 'libsodium.a')
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def reset():
|
||||
Settings.reset_auto_profile()
|
||||
|
||||
@ -484,9 +469,6 @@ def main():
|
||||
elif arg == '--help':
|
||||
print('Usage:\ntoxygen path_to_profile\ntoxygen tox_id\ntoxygen --version\ntoxygen --reset')
|
||||
return
|
||||
elif arg == '--configure':
|
||||
configure()
|
||||
return
|
||||
elif arg == '--clean':
|
||||
clean()
|
||||
return
|
||||
|
@ -22,51 +22,47 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
|
||||
if settings.Settings.get_instance()['show_welcome_screen']:
|
||||
self.ws = WelcomeScreen()
|
||||
|
||||
def setup_menu(self, Form):
|
||||
box = QtWidgets.QHBoxLayout()
|
||||
box.setContentsMargins(0, 0, 0, 0)
|
||||
box.setAlignment(QtCore.Qt.AlignLeft)
|
||||
self.profile_button = MainMenuButton(Form)
|
||||
box.addWidget(self.profile_button)
|
||||
self.settings_button = MainMenuButton(Form)
|
||||
box.addWidget(self.settings_button)
|
||||
self.plugins_button = MainMenuButton(Form)
|
||||
box.addWidget(self.plugins_button)
|
||||
self.about_button = MainMenuButton(Form)
|
||||
box.addWidget(self.about_button)
|
||||
box.setSpacing(0)
|
||||
def setup_menu(self, window):
|
||||
self.menubar = QtWidgets.QMenuBar(window)
|
||||
self.menubar.setObjectName("menubar")
|
||||
self.menubar.setNativeMenuBar(False)
|
||||
self.menubar.setMinimumSize(self.width(), 25)
|
||||
self.menubar.setMaximumSize(self.width(), 25)
|
||||
self.menubar.setBaseSize(self.width(), 25)
|
||||
self.menuProfile = QtWidgets.QMenu(self.menubar)
|
||||
|
||||
self.menuProfile = QtWidgets.QMenu()
|
||||
self.menuProfile = QtWidgets.QMenu(self.menubar)
|
||||
self.menuProfile.setObjectName("menuProfile")
|
||||
self.menuSettings = QtWidgets.QMenu()
|
||||
self.menuSettings = QtWidgets.QMenu(self.menubar)
|
||||
self.menuSettings.setObjectName("menuSettings")
|
||||
self.menuPlugins = QtWidgets.QMenu()
|
||||
self.menuPlugins = QtWidgets.QMenu(self.menubar)
|
||||
self.menuPlugins.setObjectName("menuPlugins")
|
||||
self.menuAbout = QtWidgets.QMenu()
|
||||
self.menuAbout = QtWidgets.QMenu(self.menubar)
|
||||
self.menuAbout.setObjectName("menuAbout")
|
||||
|
||||
self.actionAdd_friend = QtWidgets.QAction(Form)
|
||||
self.actionAdd_friend = QtWidgets.QAction(window)
|
||||
self.actionAdd_friend.setObjectName("actionAdd_friend")
|
||||
self.actionprofilesettings = QtWidgets.QAction(Form)
|
||||
self.actionprofilesettings = QtWidgets.QAction(window)
|
||||
self.actionprofilesettings.setObjectName("actionprofilesettings")
|
||||
self.actionPrivacy_settings = QtWidgets.QAction(Form)
|
||||
self.actionPrivacy_settings = QtWidgets.QAction(window)
|
||||
self.actionPrivacy_settings.setObjectName("actionPrivacy_settings")
|
||||
self.actionInterface_settings = QtWidgets.QAction(Form)
|
||||
self.actionInterface_settings = QtWidgets.QAction(window)
|
||||
self.actionInterface_settings.setObjectName("actionInterface_settings")
|
||||
self.actionNotifications = QtWidgets.QAction(Form)
|
||||
self.actionNotifications = QtWidgets.QAction(window)
|
||||
self.actionNotifications.setObjectName("actionNotifications")
|
||||
self.actionNetwork = QtWidgets.QAction(Form)
|
||||
self.actionNetwork = QtWidgets.QAction(window)
|
||||
self.actionNetwork.setObjectName("actionNetwork")
|
||||
self.actionAbout_program = QtWidgets.QAction(Form)
|
||||
self.actionAbout_program = QtWidgets.QAction(window)
|
||||
self.actionAbout_program.setObjectName("actionAbout_program")
|
||||
self.updateSettings = QtWidgets.QAction(Form)
|
||||
self.actionSettings = QtWidgets.QAction(Form)
|
||||
self.updateSettings = QtWidgets.QAction(window)
|
||||
self.actionSettings = QtWidgets.QAction(window)
|
||||
self.actionSettings.setObjectName("actionSettings")
|
||||
self.audioSettings = QtWidgets.QAction(Form)
|
||||
self.pluginData = QtWidgets.QAction(Form)
|
||||
self.importPlugin = QtWidgets.QAction(Form)
|
||||
self.reloadPlugins = QtWidgets.QAction(Form)
|
||||
self.lockApp = QtWidgets.QAction(Form)
|
||||
self.audioSettings = QtWidgets.QAction(window)
|
||||
self.videoSettings = QtWidgets.QAction(window)
|
||||
self.pluginData = QtWidgets.QAction(window)
|
||||
self.importPlugin = QtWidgets.QAction(window)
|
||||
self.reloadPlugins = QtWidgets.QAction(window)
|
||||
self.lockApp = QtWidgets.QAction(window)
|
||||
self.menuProfile.addAction(self.actionAdd_friend)
|
||||
self.menuProfile.addAction(self.actionSettings)
|
||||
self.menuProfile.addAction(self.lockApp)
|
||||
@ -75,16 +71,17 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
|
||||
self.menuSettings.addAction(self.actionNotifications)
|
||||
self.menuSettings.addAction(self.actionNetwork)
|
||||
self.menuSettings.addAction(self.audioSettings)
|
||||
self.menuSettings.addAction(self.videoSettings)
|
||||
self.menuSettings.addAction(self.updateSettings)
|
||||
self.menuPlugins.addAction(self.pluginData)
|
||||
self.menuPlugins.addAction(self.importPlugin)
|
||||
self.menuPlugins.addAction(self.reloadPlugins)
|
||||
self.menuAbout.addAction(self.actionAbout_program)
|
||||
|
||||
self.profile_button.setMenu(self.menuProfile)
|
||||
self.settings_button.setMenu(self.menuSettings)
|
||||
self.plugins_button.setMenu(self.menuPlugins)
|
||||
self.about_button.setMenu(self.menuAbout)
|
||||
self.menubar.addAction(self.menuProfile.menuAction())
|
||||
self.menubar.addAction(self.menuSettings.menuAction())
|
||||
self.menubar.addAction(self.menuPlugins.menuAction())
|
||||
self.menubar.addAction(self.menuAbout.menuAction())
|
||||
|
||||
self.actionAbout_program.triggered.connect(self.about_program)
|
||||
self.actionNetwork.triggered.connect(self.network_settings)
|
||||
@ -94,15 +91,13 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
|
||||
self.actionInterface_settings.triggered.connect(self.interface_settings)
|
||||
self.actionNotifications.triggered.connect(self.notification_settings)
|
||||
self.audioSettings.triggered.connect(self.audio_settings)
|
||||
self.videoSettings.triggered.connect(self.video_settings)
|
||||
self.updateSettings.triggered.connect(self.update_settings)
|
||||
self.pluginData.triggered.connect(self.plugins_menu)
|
||||
self.lockApp.triggered.connect(self.lock_app)
|
||||
self.importPlugin.triggered.connect(self.import_plugin)
|
||||
self.reloadPlugins.triggered.connect(self.reload_plugins)
|
||||
|
||||
Form.setLayout(box)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def languageChange(self, *args, **kwargs):
|
||||
self.retranslateUi()
|
||||
|
||||
@ -114,11 +109,11 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
|
||||
|
||||
def retranslateUi(self):
|
||||
self.lockApp.setText(QtWidgets.QApplication.translate("MainWindow", "Lock"))
|
||||
self.plugins_button.setText(QtWidgets.QApplication.translate("MainWindow", "Plugins"))
|
||||
self.menuPlugins.setTitle(QtWidgets.QApplication.translate("MainWindow", "Plugins"))
|
||||
self.pluginData.setText(QtWidgets.QApplication.translate("MainWindow", "List of plugins"))
|
||||
self.profile_button.setText(QtWidgets.QApplication.translate("MainWindow", "Profile"))
|
||||
self.settings_button.setText(QtWidgets.QApplication.translate("MainWindow", "Settings"))
|
||||
self.about_button.setText(QtWidgets.QApplication.translate("MainWindow", "About"))
|
||||
self.menuProfile.setTitle(QtWidgets.QApplication.translate("MainWindow", "Profile"))
|
||||
self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings"))
|
||||
self.menuAbout.setTitle(QtWidgets.QApplication.translate("MainWindow", "About"))
|
||||
self.actionAdd_friend.setText(QtWidgets.QApplication.translate("MainWindow", "Add contact"))
|
||||
self.actionprofilesettings.setText(QtWidgets.QApplication.translate("MainWindow", "Profile"))
|
||||
self.actionPrivacy_settings.setText(QtWidgets.QApplication.translate("MainWindow", "Privacy"))
|
||||
@ -128,6 +123,7 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
|
||||
self.actionAbout_program.setText(QtWidgets.QApplication.translate("MainWindow", "About program"))
|
||||
self.actionSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Settings"))
|
||||
self.audioSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Audio"))
|
||||
self.videoSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Video"))
|
||||
self.updateSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Updates"))
|
||||
self.contact_name.setPlaceholderText(QtWidgets.QApplication.translate("MainWindow", "Search"))
|
||||
self.sendMessageButton.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Send message"))
|
||||
@ -379,12 +375,8 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
|
||||
self.close()
|
||||
|
||||
def resizeEvent(self, *args, **kwargs):
|
||||
if platform.system() == 'Windows':
|
||||
self.messages.setGeometry(0, 0, self.width() - 270, self.height() - 155)
|
||||
self.friends_list.setGeometry(0, 0, 270, self.height() - 125)
|
||||
else:
|
||||
self.messages.setGeometry(0, 0, self.width() - 270, self.height() - 159)
|
||||
self.friends_list.setGeometry(0, 0, 270, self.height() - 129)
|
||||
self.messages.setGeometry(0, 0, self.width() - 270, self.height() - 155)
|
||||
self.friends_list.setGeometry(0, 0, 270, self.height() - 125)
|
||||
|
||||
self.videocallButton.setGeometry(QtCore.QRect(self.width() - 330, 10, 50, 50))
|
||||
self.callButton.setGeometry(QtCore.QRect(self.width() - 390, 10, 50, 50))
|
||||
@ -459,6 +451,10 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
|
||||
self.audio_s = AudioSettings()
|
||||
self.audio_s.show()
|
||||
|
||||
def video_settings(self):
|
||||
self.video_s = VideoSettings()
|
||||
self.video_s.show()
|
||||
|
||||
def update_settings(self):
|
||||
self.update_s = UpdateSettings()
|
||||
self.update_s.show()
|
||||
@ -559,14 +555,15 @@ class MainWindow(QtWidgets.QMainWindow, Singleton):
|
||||
def call_finished(self):
|
||||
self.update_call_state('call')
|
||||
|
||||
def update_call_state(self, fl):
|
||||
# TODO: do smth with video call button
|
||||
def update_call_state(self, state):
|
||||
os.chdir(curr_directory() + '/images/')
|
||||
pixmap = QtGui.QPixmap(curr_directory() + '/images/{}.png'.format(fl))
|
||||
|
||||
pixmap = QtGui.QPixmap(curr_directory() + '/images/{}.png'.format(state))
|
||||
icon = QtGui.QIcon(pixmap)
|
||||
self.callButton.setIcon(icon)
|
||||
self.callButton.setIconSize(QtCore.QSize(50, 50))
|
||||
pixmap = QtGui.QPixmap(curr_directory() + '/images/videocall.png')
|
||||
|
||||
pixmap = QtGui.QPixmap(curr_directory() + '/images/{}_video.png'.format(state))
|
||||
icon = QtGui.QIcon(pixmap)
|
||||
self.videocallButton.setIcon(icon)
|
||||
self.videocallButton.setIconSize(QtCore.QSize(35, 35))
|
||||
|
@ -1,5 +1,5 @@
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from widgets import RubberBand, create_menu, QRightClickButton, CenteredWidget, LineEdit
|
||||
from widgets import RubberBandWindow, create_menu, QRightClickButton, CenteredWidget, LineEdit
|
||||
from profile import Profile
|
||||
import smileys
|
||||
import util
|
||||
@ -71,38 +71,12 @@ class MessageArea(QtWidgets.QPlainTextEdit):
|
||||
self.insertPlainText(text)
|
||||
|
||||
|
||||
class ScreenShotWindow(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ScreenShotWindow, self).__init__()
|
||||
self.parent = parent
|
||||
self.setMouseTracking(True)
|
||||
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
|
||||
self.showFullScreen()
|
||||
self.setWindowOpacity(0.5)
|
||||
self.rubberband = RubberBand()
|
||||
self.rubberband.setWindowFlags(self.rubberband.windowFlags() | QtCore.Qt.FramelessWindowHint)
|
||||
self.rubberband.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
class ScreenShotWindow(RubberBandWindow):
|
||||
|
||||
def closeEvent(self, *args):
|
||||
if self.parent.isHidden():
|
||||
self.parent.show()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.origin = event.pos()
|
||||
self.rubberband.setGeometry(QtCore.QRect(self.origin, QtCore.QSize()))
|
||||
self.rubberband.show()
|
||||
QtWidgets.QWidget.mousePressEvent(self, event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self.rubberband.isVisible():
|
||||
self.rubberband.setGeometry(QtCore.QRect(self.origin, event.pos()).normalized())
|
||||
left = QtGui.QRegion(QtCore.QRect(0, 0, self.rubberband.x(), self.height()))
|
||||
right = QtGui.QRegion(QtCore.QRect(self.rubberband.x() + self.rubberband.width(), 0, self.width(), self.height()))
|
||||
top = QtGui.QRegion(0, 0, self.width(), self.rubberband.y())
|
||||
bottom = QtGui.QRegion(0, self.rubberband.y() + self.rubberband.height(), self.width(), self.height())
|
||||
self.setMask(left + right + top + bottom)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self.rubberband.isVisible():
|
||||
self.rubberband.hide()
|
||||
@ -121,13 +95,6 @@ class ScreenShotWindow(QtWidgets.QWidget):
|
||||
Profile.get_instance().send_screenshot(bytes(byte_array.data()))
|
||||
self.close()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == QtCore.Qt.Key_Escape:
|
||||
self.rubberband.setHidden(True)
|
||||
self.close()
|
||||
else:
|
||||
super(ScreenShotWindow, self).keyPressEvent(event)
|
||||
|
||||
|
||||
class SmileyWindow(QtWidgets.QWidget):
|
||||
"""
|
||||
@ -153,7 +120,7 @@ class SmileyWindow(QtWidgets.QWidget):
|
||||
for i in range(self.page_count): # buttons with smileys
|
||||
elem = QtWidgets.QRadioButton(self)
|
||||
elem.setGeometry(QtCore.QRect(i * 20 + 5, 180, 20, 20))
|
||||
elem.clicked.connect(lambda i=i: self.checked(i))
|
||||
elem.clicked.connect(lambda c, t=i: self.checked(t))
|
||||
self.radio.append(elem)
|
||||
width = max(self.page_count * 20 + 30, (self.page_size + 5) * 8 // 10)
|
||||
self.setMaximumSize(width, 200)
|
||||
@ -162,7 +129,7 @@ class SmileyWindow(QtWidgets.QWidget):
|
||||
for i in range(self.page_size): # pages - radio buttons
|
||||
b = QtWidgets.QPushButton(self)
|
||||
b.setGeometry(QtCore.QRect((i // 8) * 20 + 5, (i % 8) * 20, 20, 20))
|
||||
b.clicked.connect(lambda i=i: self.clicked(i))
|
||||
b.clicked.connect(lambda c, t=i: self.clicked(t))
|
||||
self.buttons.append(b)
|
||||
self.checked(0)
|
||||
|
||||
@ -207,8 +174,8 @@ class DropdownMenu(QtWidgets.QWidget):
|
||||
super(DropdownMenu, self).__init__(parent)
|
||||
self.installEventFilter(self)
|
||||
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
|
||||
self.setMaximumSize(180, 120)
|
||||
self.setMinimumSize(180, 120)
|
||||
self.setMaximumSize(120, 120)
|
||||
self.setMinimumSize(120, 120)
|
||||
self.screenshotButton = QRightClickButton(self)
|
||||
self.screenshotButton.setGeometry(QtCore.QRect(0, 60, 60, 60))
|
||||
self.screenshotButton.setObjectName("screenshotButton")
|
||||
@ -217,15 +184,9 @@ class DropdownMenu(QtWidgets.QWidget):
|
||||
self.fileTransferButton.setGeometry(QtCore.QRect(60, 60, 60, 60))
|
||||
self.fileTransferButton.setObjectName("fileTransferButton")
|
||||
|
||||
self.audioMessageButton = QtWidgets.QPushButton(self)
|
||||
self.audioMessageButton.setGeometry(QtCore.QRect(120, 60, 60, 60))
|
||||
|
||||
self.smileyButton = QtWidgets.QPushButton(self)
|
||||
self.smileyButton.setGeometry(QtCore.QRect(0, 0, 60, 60))
|
||||
|
||||
self.videoMessageButton = QtWidgets.QPushButton(self)
|
||||
self.videoMessageButton.setGeometry(QtCore.QRect(120, 0, 60, 60))
|
||||
|
||||
self.stickerButton = QtWidgets.QPushButton(self)
|
||||
self.stickerButton.setGeometry(QtCore.QRect(60, 0, 60, 60))
|
||||
|
||||
@ -233,22 +194,17 @@ class DropdownMenu(QtWidgets.QWidget):
|
||||
icon = QtGui.QIcon(pixmap)
|
||||
self.fileTransferButton.setIcon(icon)
|
||||
self.fileTransferButton.setIconSize(QtCore.QSize(50, 50))
|
||||
|
||||
pixmap = QtGui.QPixmap(util.curr_directory() + '/images/screenshot.png')
|
||||
icon = QtGui.QIcon(pixmap)
|
||||
self.screenshotButton.setIcon(icon)
|
||||
self.screenshotButton.setIconSize(QtCore.QSize(50, 60))
|
||||
pixmap = QtGui.QPixmap(util.curr_directory() + '/images/audio_message.png')
|
||||
icon = QtGui.QIcon(pixmap)
|
||||
self.audioMessageButton.setIcon(icon)
|
||||
self.audioMessageButton.setIconSize(QtCore.QSize(50, 50))
|
||||
|
||||
pixmap = QtGui.QPixmap(util.curr_directory() + '/images/smiley.png')
|
||||
icon = QtGui.QIcon(pixmap)
|
||||
self.smileyButton.setIcon(icon)
|
||||
self.smileyButton.setIconSize(QtCore.QSize(50, 50))
|
||||
pixmap = QtGui.QPixmap(util.curr_directory() + '/images/video_message.png')
|
||||
icon = QtGui.QIcon(pixmap)
|
||||
self.videoMessageButton.setIcon(icon)
|
||||
self.videoMessageButton.setIconSize(QtCore.QSize(55, 55))
|
||||
|
||||
pixmap = QtGui.QPixmap(util.curr_directory() + '/images/sticker.png')
|
||||
icon = QtGui.QIcon(pixmap)
|
||||
self.stickerButton.setIcon(icon)
|
||||
@ -256,8 +212,6 @@ class DropdownMenu(QtWidgets.QWidget):
|
||||
|
||||
self.screenshotButton.setToolTip(QtWidgets.QApplication.translate("MenuWindow", "Send screenshot"))
|
||||
self.fileTransferButton.setToolTip(QtWidgets.QApplication.translate("MenuWindow", "Send file"))
|
||||
self.audioMessageButton.setToolTip(QtWidgets.QApplication.translate("MenuWindow", "Send audio message"))
|
||||
self.videoMessageButton.setToolTip(QtWidgets.QApplication.translate("MenuWindow", "Send video message"))
|
||||
self.smileyButton.setToolTip(QtWidgets.QApplication.translate("MenuWindow", "Add smiley"))
|
||||
self.stickerButton.setToolTip(QtWidgets.QApplication.translate("MenuWindow", "Send sticker"))
|
||||
|
||||
@ -270,7 +224,7 @@ class DropdownMenu(QtWidgets.QWidget):
|
||||
def leaveEvent(self, event):
|
||||
self.close()
|
||||
|
||||
def eventFilter(self, object, event):
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() == QtCore.QEvent.WindowDeactivate:
|
||||
self.close()
|
||||
return False
|
||||
@ -342,31 +296,34 @@ class WelcomeScreen(CenteredWidget):
|
||||
text = QtWidgets.QApplication.translate('WelcomeScreen', 'Press Esc if you want hide app to tray.')
|
||||
elif num == 1:
|
||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
||||
'Right click on screenshot button hides app to tray during screenshot.')
|
||||
'Right click on screenshot button hides app to tray during screenshot.')
|
||||
elif num == 2:
|
||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
||||
'You can use Tox over Tor. For more info read <a href="https://wiki.tox.chat/users/tox_over_tor_tot">this post</a>')
|
||||
'You can use Tox over Tor. For more info read <a href="https://wiki.tox.chat/users/tox_over_tor_tot">this post</a>')
|
||||
elif num == 3:
|
||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
||||
'Use Settings -> Interface to customize interface.')
|
||||
'Use Settings -> Interface to customize interface.')
|
||||
elif num == 4:
|
||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
||||
'Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings.')
|
||||
'Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings.')
|
||||
elif num == 5:
|
||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
||||
'Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/xveduk/toxygen/blob/master/docs/plugins.md">Read more</a>')
|
||||
elif num in (6, 7):
|
||||
'Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md">Read more</a>')
|
||||
elif num == 6:
|
||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
||||
'Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later.')
|
||||
'Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later.')
|
||||
elif num == 7:
|
||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
||||
'New in Toxygen 0.3.0:<br>Video calls<br>Python3.6 support<br>Migration to PyQt5')
|
||||
elif num == 8:
|
||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
||||
'Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu')
|
||||
'Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu')
|
||||
elif num == 9:
|
||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
||||
'Use right click on inline image to save it')
|
||||
'Use right click on inline image to save it')
|
||||
else:
|
||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
||||
'Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam.')
|
||||
'Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam.')
|
||||
self.text.setHtml(text)
|
||||
self.checkbox.stateChanged.connect(self.not_show)
|
||||
QtCore.QTimer.singleShot(1000, self.show)
|
||||
|
163
toxygen/menu.py
@ -2,7 +2,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from settings import *
|
||||
from profile import Profile
|
||||
from util import curr_directory, copy
|
||||
from widgets import CenteredWidget, DataLabel, LineEdit
|
||||
from widgets import CenteredWidget, DataLabel, LineEdit, RubberBandWindow
|
||||
import pyaudio
|
||||
import toxes
|
||||
import plugin_support
|
||||
@ -247,12 +247,12 @@ class ProfileSettings(CenteredWidget):
|
||||
|
||||
def set_avatar(self):
|
||||
choose = QtWidgets.QApplication.translate("ProfileSettingsForm", "Choose avatar")
|
||||
name = QtWidgets.QFileDialog.getOpenFileName(self, choose, None, None, 'Images (*.png)',
|
||||
QtWidgets.QFileDialog.DontUseNativeDialog)
|
||||
name = QtWidgets.QFileDialog.getOpenFileName(self, choose, None, 'Images (*.png)',
|
||||
options=QtWidgets.QFileDialog.DontUseNativeDialog)
|
||||
if name[0]:
|
||||
bitmap = QtGui.QPixmap(name[0])
|
||||
bitmap.scaled(128, 128, QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation)
|
||||
bitmap.scaled(QtCore.QSize(128, 128), aspectRatioMode=QtCore.Qt.KeepAspectRatio,
|
||||
transformMode=QtCore.Qt.SmoothTransformation)
|
||||
|
||||
byte_array = QtCore.QByteArray()
|
||||
buffer = QtCore.QBuffer(byte_array)
|
||||
@ -261,9 +261,8 @@ class ProfileSettings(CenteredWidget):
|
||||
Profile.get_instance().set_avatar(bytes(byte_array.data()))
|
||||
|
||||
def export_profile(self):
|
||||
directory = QtWidgets.QFileDialog.getExistingDirectory(self, '', curr_directory() + '/',
|
||||
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
|
||||
|
||||
directory = QtWidgets.QFileDialog.getExistingDirectory(self, '', curr_directory(),
|
||||
QtWidgets.QFileDialog.DontUseNativeDialog) + '/'
|
||||
if directory != '/':
|
||||
reply = QtWidgets.QMessageBox.question(None,
|
||||
QtWidgets.QApplication.translate("ProfileSettingsForm",
|
||||
@ -494,8 +493,7 @@ class PrivacySettings(CenteredWidget):
|
||||
settings.save()
|
||||
|
||||
def new_path(self):
|
||||
directory = QtWidgets.QFileDialog.getExistingDirectory(self, '', curr_directory() + '/',
|
||||
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog) + '/'
|
||||
directory = QtWidgets.QFileDialog.getExistingDirectory(options=QtWidgets.QFileDialog.DontUseNativeDialog) + '/'
|
||||
if directory != '/':
|
||||
self.path.setPlainText(directory)
|
||||
|
||||
@ -567,11 +565,10 @@ class InterfaceSettings(CenteredWidget):
|
||||
self.label.setFont(font)
|
||||
self.themeSelect = QtWidgets.QComboBox(self)
|
||||
self.themeSelect.setGeometry(QtCore.QRect(30, 40, 120, 30))
|
||||
list_of_themes = ['dark']
|
||||
self.themeSelect.addItems(list_of_themes)
|
||||
self.themeSelect.addItems(list(settings.built_in_themes().keys()))
|
||||
theme = settings['theme']
|
||||
if theme in list_of_themes:
|
||||
index = list_of_themes.index(theme)
|
||||
if theme in settings.built_in_themes().keys():
|
||||
index = list(settings.built_in_themes().keys()).index(theme)
|
||||
else:
|
||||
index = 0
|
||||
self.themeSelect.setCurrentIndex(index)
|
||||
@ -660,10 +657,10 @@ class InterfaceSettings(CenteredWidget):
|
||||
|
||||
def import_st(self):
|
||||
directory = QtWidgets.QFileDialog.getExistingDirectory(self,
|
||||
QtWidgets.QApplication.translate("MainWindow",
|
||||
'Choose folder with sticker pack'),
|
||||
curr_directory(),
|
||||
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
|
||||
QtWidgets.QApplication.translate("MainWindow",
|
||||
'Choose folder with sticker pack'),
|
||||
curr_directory(),
|
||||
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
|
||||
|
||||
if directory:
|
||||
src = directory + '/'
|
||||
@ -706,6 +703,14 @@ class InterfaceSettings(CenteredWidget):
|
||||
def closeEvent(self, event):
|
||||
settings = Settings.get_instance()
|
||||
settings['theme'] = str(self.themeSelect.currentText())
|
||||
try:
|
||||
theme = settings['theme']
|
||||
app = QtWidgets.QApplication.instance()
|
||||
with open(curr_directory() + settings.built_in_themes()[theme]) as fl:
|
||||
style = fl.read()
|
||||
app.setStyleSheet(style)
|
||||
except IsADirectoryError:
|
||||
app.setStyleSheet('') # for default style
|
||||
settings['smileys'] = self.smileys.isChecked()
|
||||
restart = False
|
||||
if settings['mirror_mode'] != self.mirror_mode.isChecked():
|
||||
@ -797,6 +802,128 @@ class AudioSettings(CenteredWidget):
|
||||
settings.save()
|
||||
|
||||
|
||||
class DesktopAreaSelectionWindow(RubberBandWindow):
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self.rubberband.isVisible():
|
||||
self.rubberband.hide()
|
||||
rect = self.rubberband.geometry()
|
||||
width, height = rect.width(), rect.height()
|
||||
if width >= 8 and height >= 8:
|
||||
self.parent.save(rect.x(), rect.y(), width, height)
|
||||
self.close()
|
||||
|
||||
|
||||
class VideoSettings(CenteredWidget):
|
||||
"""
|
||||
Audio calls settings form
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.initUI()
|
||||
self.retranslateUi()
|
||||
self.center()
|
||||
self.desktopAreaSelection = None
|
||||
|
||||
def initUI(self):
|
||||
self.setObjectName("videoSettingsForm")
|
||||
self.resize(400, 120)
|
||||
self.setMinimumSize(QtCore.QSize(400, 120))
|
||||
self.setMaximumSize(QtCore.QSize(400, 120))
|
||||
self.in_label = QtWidgets.QLabel(self)
|
||||
self.in_label.setGeometry(QtCore.QRect(25, 5, 350, 20))
|
||||
settings = Settings.get_instance()
|
||||
font = QtGui.QFont()
|
||||
font.setPointSize(16)
|
||||
font.setBold(True)
|
||||
font.setFamily(settings['font'])
|
||||
self.in_label.setFont(font)
|
||||
self.video_size = QtWidgets.QComboBox(self)
|
||||
self.video_size.setGeometry(QtCore.QRect(25, 70, 350, 30))
|
||||
self.input = QtWidgets.QComboBox(self)
|
||||
self.input.setGeometry(QtCore.QRect(25, 30, 350, 30))
|
||||
self.input.currentIndexChanged.connect(self.selectionChanged)
|
||||
self.button = QtWidgets.QPushButton(self)
|
||||
self.button.clicked.connect(self.button_clicked)
|
||||
self.button.setGeometry(QtCore.QRect(25, 70, 350, 30))
|
||||
import cv2
|
||||
self.devices = [-1]
|
||||
screen = QtWidgets.QApplication.primaryScreen()
|
||||
size = screen.size()
|
||||
self.frame_max_sizes = [(size.width(), size.height())]
|
||||
desktop = QtWidgets.QApplication.translate("videoSettingsForm", "Desktop")
|
||||
self.input.addItem(desktop)
|
||||
for i in range(10):
|
||||
v = cv2.VideoCapture(i)
|
||||
if v.isOpened():
|
||||
v.set(cv2.CAP_PROP_FRAME_WIDTH, 10000)
|
||||
v.set(cv2.CAP_PROP_FRAME_HEIGHT, 10000)
|
||||
|
||||
width = int(v.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
height = int(v.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
del v
|
||||
self.devices.append(i)
|
||||
self.frame_max_sizes.append((width, height))
|
||||
self.input.addItem('Device #' + str(i))
|
||||
try:
|
||||
index = self.devices.index(settings.video['device'])
|
||||
self.input.setCurrentIndex(index)
|
||||
except:
|
||||
print('Video devices error!')
|
||||
|
||||
def retranslateUi(self):
|
||||
self.setWindowTitle(QtWidgets.QApplication.translate("videoSettingsForm", "Video settings"))
|
||||
self.in_label.setText(QtWidgets.QApplication.translate("videoSettingsForm", "Device:"))
|
||||
self.button.setText(QtWidgets.QApplication.translate("videoSettingsForm", "Select region"))
|
||||
|
||||
def button_clicked(self):
|
||||
self.desktopAreaSelection = DesktopAreaSelectionWindow(self)
|
||||
|
||||
def closeEvent(self, event):
|
||||
try:
|
||||
settings = Settings.get_instance()
|
||||
settings.video['device'] = self.devices[self.input.currentIndex()]
|
||||
text = self.video_size.currentText()
|
||||
settings.video['width'] = int(text.split(' ')[0])
|
||||
settings.video['height'] = int(text.split(' ')[-1])
|
||||
settings.save()
|
||||
except Exception as ex:
|
||||
print('Saving video settings error: ' + str(ex))
|
||||
|
||||
def save(self, x, y, width, height):
|
||||
self.desktopAreaSelection = None
|
||||
settings = Settings.get_instance()
|
||||
settings.video['device'] = -1
|
||||
settings.video['width'] = width
|
||||
settings.video['height'] = height
|
||||
settings.video['x'] = x
|
||||
settings.video['y'] = y
|
||||
settings.save()
|
||||
|
||||
def selectionChanged(self):
|
||||
if self.input.currentIndex() == 0:
|
||||
self.button.setVisible(True)
|
||||
self.video_size.setVisible(False)
|
||||
else:
|
||||
self.button.setVisible(False)
|
||||
self.video_size.setVisible(True)
|
||||
width, height = self.frame_max_sizes[self.input.currentIndex()]
|
||||
self.video_size.clear()
|
||||
dims = [
|
||||
(320, 240),
|
||||
(640, 360),
|
||||
(640, 480),
|
||||
(720, 480),
|
||||
(1280, 720),
|
||||
(1920, 1080),
|
||||
(2560, 1440)
|
||||
]
|
||||
for w, h in dims:
|
||||
if w <= width and h <= height:
|
||||
self.video_size.addItem(str(w) + ' * ' + str(h))
|
||||
|
||||
|
||||
class PluginsSettings(CenteredWidget):
|
||||
"""
|
||||
Plugins settings form
|
||||
|
@ -14,6 +14,8 @@ import avwidgets
|
||||
import plugin_support
|
||||
import basecontact
|
||||
import items_factory
|
||||
import cv2
|
||||
import threading
|
||||
|
||||
|
||||
class Profile(basecontact.BaseContact, Singleton):
|
||||
@ -36,6 +38,7 @@ class Profile(basecontact.BaseContact, Singleton):
|
||||
self._tox = tox
|
||||
self._file_transfers = {} # dict of file transfers. key - tuple (friend_number, file_number)
|
||||
self._call = calls.AV(tox.AV) # object with data about calls
|
||||
self._call_widgets = {} # dict of incoming call widgets
|
||||
self._incoming_calls = set()
|
||||
self._load_history = True
|
||||
self._waiting_for_reconnection = False
|
||||
@ -85,6 +88,7 @@ class Profile(basecontact.BaseContact, Singleton):
|
||||
if status is not None:
|
||||
self._tox.self_set_status(status)
|
||||
elif not self._waiting_for_reconnection:
|
||||
self._waiting_for_reconnection = True
|
||||
QtCore.QTimer.singleShot(50000, self.reconnect)
|
||||
|
||||
def set_name(self, value):
|
||||
@ -659,15 +663,15 @@ class Profile(basecontact.BaseContact, Singleton):
|
||||
friend = self._contacts[num]
|
||||
name = friend.name
|
||||
dialog = QtWidgets.QApplication.translate('MainWindow',
|
||||
"Enter new alias for friend {} or leave empty to use friend's name:")
|
||||
"Enter new alias for friend {} or leave empty to use friend's name:")
|
||||
dialog = dialog.format(name)
|
||||
title = QtWidgets.QApplication.translate('MainWindow',
|
||||
'Set alias')
|
||||
text, ok = QtGui.QInputDialog.getText(None,
|
||||
title,
|
||||
dialog,
|
||||
QtWidgets.QLineEdit.Normal,
|
||||
name)
|
||||
'Set alias')
|
||||
text, ok = QtWidgets.QInputDialog.getText(None,
|
||||
title,
|
||||
dialog,
|
||||
QtWidgets.QLineEdit.Normal,
|
||||
name)
|
||||
if ok:
|
||||
settings = Settings.get_instance()
|
||||
aliases = settings['friends_aliases']
|
||||
@ -1226,10 +1230,9 @@ class Profile(basecontact.BaseContact, Singleton):
|
||||
self._messages.scrollToBottom()
|
||||
else:
|
||||
friend.actions = True
|
||||
# TODO: dict of widgets
|
||||
self._call_widget = avwidgets.IncomingCallWidget(friend_number, text, friend.name)
|
||||
self._call_widget.set_pixmap(friend.get_pixmap())
|
||||
self._call_widget.show()
|
||||
self._call_widgets[friend_number] = avwidgets.IncomingCallWidget(friend_number, text, friend.name)
|
||||
self._call_widgets[friend_number].set_pixmap(friend.get_pixmap())
|
||||
self._call_widgets[friend_number].show()
|
||||
|
||||
def accept_call(self, friend_number, audio, video):
|
||||
"""
|
||||
@ -1239,8 +1242,7 @@ class Profile(basecontact.BaseContact, Singleton):
|
||||
self._screen.active_call()
|
||||
if friend_number in self._incoming_calls:
|
||||
self._incoming_calls.remove(friend_number)
|
||||
if hasattr(self, '_call_widget'):
|
||||
del self._call_widget
|
||||
del self._call_widgets[friend_number]
|
||||
|
||||
def stop_call(self, friend_number, by_friend):
|
||||
"""
|
||||
@ -1254,8 +1256,9 @@ class Profile(basecontact.BaseContact, Singleton):
|
||||
self._screen.call_finished()
|
||||
self._call.finish_call(friend_number, by_friend) # finish or decline call
|
||||
if hasattr(self, '_call_widget'):
|
||||
self._call_widget.close()
|
||||
del self._call_widget
|
||||
self._call_widget[friend_number].close()
|
||||
del self._call_widget[friend_number]
|
||||
threading.Timer(2.0, lambda: cv2.destroyWindow(str(friend_number))).start()
|
||||
friend = self.get_friend_by_number(friend_number)
|
||||
friend.append_message(InfoMessage(text, time.time()))
|
||||
if friend_number == self.get_active_number():
|
||||
|
22
toxygen/screen_sharing.py
Normal file
@ -0,0 +1,22 @@
|
||||
import numpy as np
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
|
||||
class DesktopGrabber:
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
self._x = x
|
||||
self._y = y
|
||||
self._width = width
|
||||
self._height = height
|
||||
self._width -= width % 4
|
||||
self._height -= height % 4
|
||||
self._screen = QtWidgets.QApplication.primaryScreen()
|
||||
|
||||
def read(self):
|
||||
pixmap = self._screen.grabWindow(0, self._x, self._y, self._width, self._height)
|
||||
image = pixmap.toImage()
|
||||
s = image.bits().asstring(self._width * self._height * 4)
|
||||
arr = np.fromstring(s, dtype=np.uint8).reshape((self._height, self._width, 4))
|
||||
|
||||
return True, arr
|
@ -47,6 +47,7 @@ class Settings(dict, Singleton):
|
||||
self.audio = {'input': p.get_default_input_device_info()['index'] if input_devices else -1,
|
||||
'output': p.get_default_output_device_info()['index'] if output_devices else -1,
|
||||
'enabled': input_devices and output_devices}
|
||||
self.video = {'device': -1, 'width': 640, 'height': 480, 'x': 0, 'y': 0}
|
||||
|
||||
@staticmethod
|
||||
def get_auto_profile():
|
||||
@ -56,7 +57,10 @@ class Settings(dict, Singleton):
|
||||
data = fl.read()
|
||||
auto = json.loads(data)
|
||||
if 'path' in auto and 'name' in auto:
|
||||
return str(auto['path']), str(auto['name'])
|
||||
path = str(auto['path'])
|
||||
name = str(auto['name'])
|
||||
if os.path.isfile(append_slash(path) + name + '.tox'):
|
||||
return path, name
|
||||
return '', ''
|
||||
|
||||
@staticmethod
|
||||
@ -99,7 +103,7 @@ class Settings(dict, Singleton):
|
||||
Default profile settings
|
||||
"""
|
||||
return {
|
||||
'theme': 'default',
|
||||
'theme': 'dark',
|
||||
'ipv6_enabled': True,
|
||||
'udp_enabled': True,
|
||||
'proxy_type': 0,
|
||||
@ -147,8 +151,16 @@ class Settings(dict, Singleton):
|
||||
def supported_languages():
|
||||
return {
|
||||
'English': 'en_EN',
|
||||
'French': 'fr_FR',
|
||||
'Russian': 'ru_RU',
|
||||
'French': 'fr_FR'
|
||||
'Ukrainian': 'uk_UA'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def built_in_themes():
|
||||
return {
|
||||
'dark': '/styles/dark_style.qss',
|
||||
'default': '/styles/style.qss'
|
||||
}
|
||||
|
||||
def upgrade(self):
|
||||
|
1324
toxygen/styles/dark_style.qss
Normal file
@ -41,6 +41,9 @@
|
||||
<file>rc/radio_unchecked.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="qdarkstyle">
|
||||
<file>dark_style.qss</file>
|
||||
</qresource>
|
||||
<qresource prefix="defaultstyle">
|
||||
<file>style.qss</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from ctypes import c_char_p, Structure, c_bool, byref, c_int, c_size_t, POINTER, c_uint16, c_void_p, c_uint64
|
||||
from ctypes import create_string_buffer, ArgumentError, CFUNCTYPE, c_uint32, sizeof, c_uint8
|
||||
from toxcore_enums_and_consts import *
|
||||
|
@ -30,7 +30,8 @@ def tox_dns(email):
|
||||
netman.setProxy(proxy)
|
||||
for url in urls:
|
||||
try:
|
||||
request = QtNetwork.QNetworkRequest(url)
|
||||
request = QtNetwork.QNetworkRequest()
|
||||
request.setUrl(QtCore.QUrl(url))
|
||||
request.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader, "application/json")
|
||||
reply = netman.post(request, bytes(json.dumps(data), 'utf-8'))
|
||||
|
||||
|
@ -1,2 +1,2 @@
|
||||
SOURCES = main.py profile.py menu.py list_items.py loginscreen.py mainscreen.py plugins/plugin_super_class.py callbacks.py widgets.py avwidgets.py mainscreen_widgets.py passwordscreen.py
|
||||
TRANSLATIONS = translations/en_GB.ts translations/ru_RU.ts translations/fr_FR.ts
|
||||
TRANSLATIONS = translations/en_GB.ts translations/ru_RU.ts translations/fr_FR.ts translations/uk_UA.ts
|
||||
|
BIN
toxygen/translations/uk_UA.qm
Normal file
1247
toxygen/translations/uk_UA.ts
Normal file
@ -57,7 +57,10 @@ def get_url(version):
|
||||
|
||||
def get_params(url, version):
|
||||
if is_from_sources():
|
||||
return ['python3', 'toxygen_updater.py', url, version]
|
||||
if platform.system() == 'Windows':
|
||||
return ['python', 'toxygen_updater.py', url, version]
|
||||
else:
|
||||
return ['python3', 'toxygen_updater.py', url, version]
|
||||
elif platform.system() == 'Windows':
|
||||
return [util.curr_directory() + '/toxygen_updater.exe', url, version]
|
||||
else:
|
||||
@ -87,7 +90,8 @@ def send_request(version):
|
||||
netman.setProxy(proxy)
|
||||
url = test_url(version)
|
||||
try:
|
||||
request = QtNetwork.QNetworkRequest(url)
|
||||
request = QtNetwork.QNetworkRequest()
|
||||
request.setUrl(QtCore.QUrl(url))
|
||||
reply = netman.get(request)
|
||||
while not reply.isFinished():
|
||||
QtCore.QThread.msleep(1)
|
||||
|
@ -4,14 +4,32 @@ import shutil
|
||||
import sys
|
||||
import re
|
||||
|
||||
program_version = '0.2.8'
|
||||
|
||||
program_version = '0.3.1'
|
||||
|
||||
|
||||
def cached(func):
|
||||
saved_result = None
|
||||
|
||||
def wrapped_func():
|
||||
nonlocal saved_result
|
||||
if saved_result is None:
|
||||
saved_result = func()
|
||||
|
||||
return saved_result
|
||||
|
||||
return wrapped_func
|
||||
|
||||
|
||||
def log(data):
|
||||
with open(curr_directory() + '/logs.log', 'a') as fl:
|
||||
fl.write(str(data) + '\n')
|
||||
try:
|
||||
with open(curr_directory() + '/logs.log', 'a') as fl:
|
||||
fl.write(str(data) + '\n')
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@cached
|
||||
def curr_directory():
|
||||
return os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
@ -46,9 +64,8 @@ def convert_time(t):
|
||||
return '%02d:%02d' % (h, m)
|
||||
|
||||
|
||||
@cached
|
||||
def time_offset():
|
||||
if hasattr(time_offset, 'offset'):
|
||||
return time_offset.offset
|
||||
hours = int(time.strftime('%H'))
|
||||
minutes = int(time.strftime('%M'))
|
||||
sec = int(time.time()) - time.timezone
|
||||
@ -56,7 +73,6 @@ def time_offset():
|
||||
h, m = divmod(m, 60)
|
||||
d, h = divmod(h, 24)
|
||||
result = hours * 60 + minutes - h * 60 - m
|
||||
time_offset.offset = result
|
||||
return result
|
||||
|
||||
|
||||
@ -66,6 +82,7 @@ def append_slash(s):
|
||||
return s
|
||||
|
||||
|
||||
@cached
|
||||
def is_64_bit():
|
||||
return sys.maxsize > 2 ** 32
|
||||
|
||||
|
@ -77,6 +77,42 @@ class RubberBand(QtWidgets.QRubberBand):
|
||||
self.painter.end()
|
||||
|
||||
|
||||
class RubberBandWindow(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__()
|
||||
self.parent = parent
|
||||
self.setMouseTracking(True)
|
||||
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
|
||||
self.showFullScreen()
|
||||
self.setWindowOpacity(0.5)
|
||||
self.rubberband = RubberBand()
|
||||
self.rubberband.setWindowFlags(self.rubberband.windowFlags() | QtCore.Qt.FramelessWindowHint)
|
||||
self.rubberband.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.origin = event.pos()
|
||||
self.rubberband.setGeometry(QtCore.QRect(self.origin, QtCore.QSize()))
|
||||
self.rubberband.show()
|
||||
QtWidgets.QWidget.mousePressEvent(self, event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self.rubberband.isVisible():
|
||||
self.rubberband.setGeometry(QtCore.QRect(self.origin, event.pos()).normalized())
|
||||
left = QtGui.QRegion(QtCore.QRect(0, 0, self.rubberband.x(), self.height()))
|
||||
right = QtGui.QRegion(QtCore.QRect(self.rubberband.x() + self.rubberband.width(), 0, self.width(), self.height()))
|
||||
top = QtGui.QRegion(0, 0, self.width(), self.rubberband.y())
|
||||
bottom = QtGui.QRegion(0, self.rubberband.y() + self.rubberband.height(), self.width(), self.height())
|
||||
self.setMask(left + right + top + bottom)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == QtCore.Qt.Key_Escape:
|
||||
self.rubberband.setHidden(True)
|
||||
self.close()
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
|
||||
|
||||
def create_menu(menu):
|
||||
"""
|
||||
:return translated menu
|
||||
@ -128,4 +164,3 @@ class MultilineEdit(CenteredWidget):
|
||||
def button_click(self):
|
||||
self.save(self.edit.toPlainText())
|
||||
self.close()
|
||||
|
||||
|