diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e31cfb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +*.user diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..316adb2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "QJoysticks"] + path = QJoysticks + url = https://github.com/alex-spataru/QJoysticks diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a1a9ae..8caacc4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,27 +7,28 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_REQUIRED_FLAGS -std=c++17) -set(OpenCV_DIR C:/Libraries/LibOpenCV) - - +find_package(SDL2 REQUIRED) find_package(OpenCV REQUIRED) find_package(Threads) -find_package(Qt5 REQUIRED COMPONENTS +find_package(Qt6 REQUIRED COMPONENTS Core Widgets - SerialPort WebSockets - WebEngine + WebEngineQuick PrintSupport Qml Quick + QuickControls2 Gui - Gamepad Concurrent) + add_executable(${PROJECT_NAME} WIN32 resources/icon.rc) -#add_executable(${PROJECT_NAME} resources/icon.rc) + +file(GLOB QJoystick "${QJoy}/src/*.cpp" "${QJoy}/src/*.h" + "${QJoy}/src/QJoysticks/*.cpp" "${QJoy}/src/QJoysticks/*.h" +) target_sources(${PROJECT_NAME} PRIVATE sources/main.cpp @@ -67,8 +68,6 @@ target_sources(${PROJECT_NAME} PRIVATE sources/RemoteController.hxx PRIVATE sources/QmlImageItem.cpp PRIVATE sources/QmlImageItem.hxx - PRIVATE sources/Gamepad.cpp - PRIVATE sources/Gamepad.hxx PRIVATE sources/EditorIndenter.cpp PRIVATE sources/EditorIndenter.hxx PRIVATE sources/SettingsController.cpp @@ -80,7 +79,11 @@ target_sources(${PROJECT_NAME} PRIVATE sources/HintBase.cpp PRIVATE sources/HintBase.hxx PRIVATE sources/ApiTokenDialog.cpp - PRIVATE sources/ApiTokenDialog.hxx) + PRIVATE sources/ApiTokenDialog.hxx + PRIVATE sources/Joystick.cpp + PRIVATE sources/Joystick.hxx + PRIVATE ${QJoystick} + ) if (WIN32) target_sources(${PROJECT_NAME} @@ -93,18 +96,27 @@ endif() target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) +target_include_directories(${PROJECT_NAME} PRIVATE + ${SDL2_DIR}/include + ${QJoy}/src +) + +target_compile_definitions(${PROJECT_NAME} PRIVATE + SDL_SUPPORTED +) + target_link_libraries(${PROJECT_NAME} - PRIVATE Qt5::SerialPort - PRIVATE Qt5::Core - PRIVATE Qt5::Widgets - PRIVATE Qt5::WebSockets - PRIVATE Qt5::PrintSupport - PRIVATE Qt5::WebEngine - PRIVATE Qt5::Qml - PRIVATE Qt5::Quick - PRIVATE Qt5::Gui - PRIVATE Qt5::Gamepad - PRIVATE Qt5::Concurrent + PRIVATE Qt6::Core + PRIVATE Qt6::Widgets + PRIVATE Qt6::WebSockets + PRIVATE Qt6::PrintSupport + PRIVATE Qt6::WebEngineQuick + PRIVATE Qt6::Qml + PRIVATE Qt6::Quick + PRIVATE Qt6::QuickControls2 + PRIVATE Qt6::Gui + PRIVATE Qt6::Concurrent PRIVATE ${OpenCV_LIBS} + ${SDL2_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) diff --git a/QJoysticks b/QJoysticks new file mode 160000 index 0000000..ccc0f53 --- /dev/null +++ b/QJoysticks @@ -0,0 +1 @@ +Subproject commit ccc0f53d086d3d5f5bc5f1586362b8eaffeb7e2d diff --git a/README.md b/README.md index cebd803..8db000e 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,4 @@ __[Загрузить (для Windows)](https://murproject.com/documents/17/murI ## Сборка -Инструкции по сборке представлены [на данной странице](https://murproject.github.io/pages/building-mur-ide). +Инструкции по сборке представлены [на данной странице](https://murproject.github.io/pages/building-mur-ide). \ No newline at end of file diff --git a/resources/config/schemes/py.json b/resources/config/schemes/py.json new file mode 100644 index 0000000..c5a8750 --- /dev/null +++ b/resources/config/schemes/py.json @@ -0,0 +1,61 @@ +{ + "scheme": { + "comment": "Python", + "suffixes": ["py"], + "highlight": [{ + "comment": "numbers", + "match": "\\b\\d+(\\.\\d*)?\\b", + "color": "#AF601A", + "bold": true + }, { + "comment": "functions", + "match": "\\b[\\w_]+\\s*(?=\\()", + "color": "#1ABC9C", + "bold": true + }, { + "comment": "keywords", + "match": "(?:\\b|\\s+)(?:(with|yield|del|import|as|global|pass|raise|in|from|try|except|pass))\\b", + "color": "#C678DD", + "bold": true + }, { + "comment": "units", + "match": "(?:\\b|\\s+)(?:(def|class|lambda))\\b", + "color": "#F1948A", + "bold": true + }, { + "comment": "logic", + "match": "(?:\\b|\\s+)(?:(is|not|if|elif|and|or|else|nonlocal))\\b", + "color": "#FFCD02", + "bold": true + }, { + "comment": "boolean", + "match": "(?:\\b|\\s+)(?:(True|False|None|assert))\\b", + "color": "#FC6E51", + "bold": true + }, { + "comment": "controll", + "match": "(?:\\b|\\s+)(?:(finally|return|break|continue|for|while))\\b", + "color": "#5D9CCE", + "bold": true + }, { + "comment": "strings", + "match": "\".*\"", + "color": "#9DA5B4" + }, { + "comment": "single quoted string", + "match": "\'.*\'", + "color": "#9DA5B4" + }, { + "comment": "inline comments", + "match": "#.*$", + "color": "#888", + "italic": true + }, + { + "comment": "multiline comments", + "match": "\"\"\".*\"\"\"", + "color": "#888", + "italic": true + }] + } +} diff --git a/resources/examples/auv_depth_preg.py b/resources/examples/auv_depth_preg.py index c4a4c12..4c80dd1 100644 --- a/resources/examples/auv_depth_preg.py +++ b/resources/examples/auv_depth_preg.py @@ -1,7 +1,7 @@ # Данный пример предназначен для работы на аппарате! # В данном примере реализован простой пропорциональный регулятор по глубине. -# Аппарат убет удерживать заданную глубину 10 секунд. +# Аппарат будет удерживать заданную глубину 10 секунд. import pymurapi as mur diff --git a/resources/examples/auv_led.py b/resources/examples/auv_led.py index 32f2ba8..31a249f 100644 --- a/resources/examples/auv_led.py +++ b/resources/examples/auv_led.py @@ -20,6 +20,6 @@ auv.set_rgb_color(0, 50, 0) time.sleep(3) # Красный - auv.set_rgb_color(0, 50, 0) + auv.set_rgb_color(50, 0, 0) time.sleep(3) diff --git a/resources/examples/auv_moving_average.py b/resources/examples/auv_moving_average.py new file mode 100644 index 0000000..8f3e172 --- /dev/null +++ b/resources/examples/auv_moving_average.py @@ -0,0 +1,57 @@ +# В данном примере реализовано сглаживание тяги методом "простое скользящее среднее" +# Простое скользящее среднее представляет собой сумму последних чисел деленную на их количество. +# Использование скользящего среднего необходимо для плавного увеличения тяги на движителях. + +import pymurapi as mur +import time + +auv = mur.mur_init() + +class MovingAverage: + _storage_size = 50 + + def __init__(self): + self._storage = [0] * self._storage_size + self._counter = 0 + + # Добавление элементов в список + def add(self, value): + self._storage[self._counter] = value + + if(self._counter < (self._storage_size - 1)): + self._counter += 1 + else: + self._counter = 0 + + # Получение среднего арифметического из списка + def get(self): + result = sum(self._storage) / self._storage_size + return result + + # Приравнивание элементов списка нулю + def clean(self): + for i in range(self._storage_size): + self._storage[i] = 0 + + +power1 = MovingAverage() +power2 = MovingAverage() +now = time.time() + +while True: + if (time.time() - now) < 7: + power1.add(-50) + power2.add(100) + elif 7 < (time.time() - now) < 15: + power1.add(50) + power2.add(-100) + else: + power1.add(0) + power2.add(0) + + p1 = power1.get() + p2 = power2.get() + auv.set_motor_power(1, p1) + auv.set_motor_power(2, p2) + + time.sleep(0.1) \ No newline at end of file diff --git a/resources/examples/auv_stream.py b/resources/examples/auv_stream.py new file mode 100644 index 0000000..67d7161 --- /dev/null +++ b/resources/examples/auv_stream.py @@ -0,0 +1,52 @@ +# Пример для версии аппаратов MiddleAUV-CM4 +# Здесь реализована трансляция видео из скрипта +# Данный пример лучше использовать только для тестирования и отладки + +import cv2 as cv +import numpy as np +import pymurapi as mur +from time import sleep + +auv = mur.mur_init() +mur_view = auv.get_videoserver() + +cap0 = cv.VideoCapture(0) +cap1 = cv.VideoCapture(1) + +def find_contours(image, color, approx = cv.CHAIN_APPROX_SIMPLE): + hsv_image = cv.cvtColor(image, cv.COLOR_BGR2HSV) + mask = cv.inRange(hsv_image, color[0], color[1]) + contours, _ = cv.findContours(mask, cv.RETR_CCOMP, approx) + return contours, mask + +def img_process(img, num=0): + font = cv.FONT_HERSHEY_PLAIN + contours, mask = find_contours(img, ((0, 0, 220), (180, 255, 255))) + + if contours: + + for contour in contours: + if cv.contourArea(contour) > 50: + rectangle = cv.minAreaRect(contour) + box = np.int0(cv.boxPoints(rectangle)) + cv.drawContours(img,[box],0,(0,0,250),2) + + cv.putText(img,'Camera {}'.format(num),(5,25),font,2,(255,255,255),2,cv.LINE_AA) + + return img, mask + +while True: + ok, frame0 = cap0.read() +# ok, frame1 = cap1.read() + + frame0 = cv.resize(frame0, (320, 240)) +# frame1 = cv.resize(frame1, (320, 240)) + + result0, mask0 = img_process(frame0, 0) +# result1, mask1 = img_process(frame1, 1) + + mur_view.show(result0, 0) + mur_view.show(mask0, 1) + +mur_view.stop() +print("done") diff --git a/resources/examples/sim_depth_preg.py b/resources/examples/sim_depth_preg.py index b570274..9ea39c9 100644 --- a/resources/examples/sim_depth_preg.py +++ b/resources/examples/sim_depth_preg.py @@ -1,7 +1,7 @@ # Данный пример предназначен для работы в симуляторе! # В данном примере реализован простой пропорциональный регулятор по глубине. -# Аппарат убет удерживать заданную глубину 10 секунд. +# Аппарат будет удерживать заданную глубину 10 секунд. import pymurapi as mur @@ -32,4 +32,4 @@ def keep_depth(depth_to_set): keep_depth(depth) if (time.time() - now) > 15: break - \ No newline at end of file + diff --git a/resources/examples/sim_hsv.py b/resources/examples/sim_hsv.py index 4ac8c92..e658d2a 100644 --- a/resources/examples/sim_hsv.py +++ b/resources/examples/sim_hsv.py @@ -5,7 +5,7 @@ # Функции get_image_front и get_image_bottom работают только в симуляторе. # Для получения видео на аппарате используйте cv2.VideoCapture из OpenCV! -# (https://docs.opencv.org/4.1.1/dd/d43/tutorial_py_video_display.html) +# (https://docs.opencv.org/4.7.0/dd/d43/tutorial_py_video_display.html) import pymurapi as mur diff --git a/resources/examples/sim_test.py b/resources/examples/sim_test.py index bb8ccba..25e3be0 100644 --- a/resources/examples/sim_test.py +++ b/resources/examples/sim_test.py @@ -5,7 +5,7 @@ # Функции get_image_front и get_image_bottom работают только в симуляторе. # Для получения видео на аппарате используйте cv2.VideoCapture из OpenCV! -# (https://docs.opencv.org/4.1.1/dd/d43/tutorial_py_video_display.html) +# (https://docs.opencv.org/4.7.0/dd/d43/tutorial_py_video_display.html) import pymurapi as mur diff --git a/resources/examples/usv_led.py b/resources/examples/usv_led.py deleted file mode 100644 index 12ce0b8..0000000 --- a/resources/examples/usv_led.py +++ /dev/null @@ -1,25 +0,0 @@ -# Данный пример предназначен для работы на аппарате! - -# В данном примере мы устанавливаем цвет RGB ленты -# c периодом включения в 1 секунду и периодом выключения в 0.5 секунды -# на 3 секунды для каждого цвета (синий, зеленый красный) . - - -import pymurapi as mur -import time - -usv = mur.usv_init() - -if __name__ == '__main__': - usv.set_on_delay(1) - usv.set_off_delay(0.5) - # Синий - usv.set_rgb_color(0, 0, 50) - time.sleep(3) - # Зеленый - usv.set_rgb_color(0, 50, 0) - time.sleep(3) - # Красный - usv.set_rgb_color(0, 50, 0) - time.sleep(3) - diff --git a/resources/examples/usv_motors_test .py b/resources/examples/usv_motors_test .py deleted file mode 100644 index 713dac6..0000000 --- a/resources/examples/usv_motors_test .py +++ /dev/null @@ -1,16 +0,0 @@ -# Данный пример предназначен для тестирования моторов на аппарате! - -# В данном примере мы подаем тягу на 4 мотора продолжительностью в 5 секунд. - - -import pymurapi as mur -import time - -usv = mur.usv_init() - -if __name__ == '__main__': - usv.set_motor_power(0, -50) - usv.set_motor_power(1, 50) - usv.set_servo(150) - time.sleep(5) - diff --git a/resources/examples/usv_yaw_preg.py b/resources/examples/usv_yaw_preg.py deleted file mode 100644 index 2f6fbe3..0000000 --- a/resources/examples/usv_yaw_preg.py +++ /dev/null @@ -1,60 +0,0 @@ -# Данный пример предназначен для работы на аппарате! - -# В данном примере реализован простой пропорциональный регулятор по курсу. -# Аппарат будет плыть прямо с тягой в 25%, удерживая изначальный курс, 10 секунд. - -import pymurapi as mur -import cv2 as cv -import time - -usv = mur.usv_init() - -# Перевод угла из -180 <=> 180 в 0 <=> 360 -def to_360(angle): - if angle > 0.0: - return angle - if angle <= 0.0: - return 360.0 + angle - -# Перевод угла >< 360 в 0 <=> 360 -def clamp_to_360(angle): - if angle < 0.0: - return angle + 360.0 - if angle > 360.0: - return angle - 360.0 - return angle - -# Перевод угла из 0 <=> 360 в -180 <=> 180 -def to_180(angle): - if angle > 180.0: - return angle - 360.0 - return angle - -# Преобразовать v в промежуток между min max -def clamp(v, min, max): - if v < min: - return min - if v > max: - return max - return v - -# Функция удержания курса -def keep_yaw(yaw_to_set, power): - current_yaw = to_360(auv.get_yaw()) - er = clamp_to_360(yaw_to_set - current_yaw) - er = to_180(er) - res = er * 0.7 - usv.set_motor_power(0, clamp(int(power - res), -100, 100)) - usv.set_motor_power(1, clamp(int(power + res), -100, 100)) - - -if __name__ == '__main__': - time.sleep(0.5) - yaw = to_360(usv.get_yaw()) - now = time.time() - while True: - keep_yaw(yaw, -25) - if (time.time() - now) > 10: - break - - diff --git a/resources/fonts/NotoMono-Regular.ttf b/resources/fonts/NotoMono-Regular.ttf new file mode 100644 index 0000000..3560a3a Binary files /dev/null and b/resources/fonts/NotoMono-Regular.ttf differ diff --git a/resources/fonts/NotoSans-Bold.ttf b/resources/fonts/NotoSans-Bold.ttf new file mode 100644 index 0000000..f95a702 Binary files /dev/null and b/resources/fonts/NotoSans-Bold.ttf differ diff --git a/resources/fonts/NotoSans-Regular.ttf b/resources/fonts/NotoSans-Regular.ttf new file mode 100644 index 0000000..7da1a0f Binary files /dev/null and b/resources/fonts/NotoSans-Regular.ttf differ diff --git a/resources/help/help.html b/resources/help/help.html index f770821..dc9988f 100644 --- a/resources/help/help.html +++ b/resources/help/help.html @@ -1,1852 +1,397 @@ - - - - -help - - -

Hello, mur!

Описание интерфейса

Интерфейс murIDE состоит из 10 основных элементов:

Подключение к аппарату

Все просто - вам нужно включить аппарат, подождать пока появится WiFi (murAP), подключится к нему (пароль по умолчанию vladivostok) и... Все готово!

Запуск симулятора

Запустить симулятор еще проще - достаточно нажать кнопку "Запуск симулятора" и подождать пока он откроется. Симулятор программируется точно так же как и аппарат, за исключением пары функций из pymurapi.

Важно: вы не можете управлять LED лентой в симуляторе. Для получения изображений из симулятора есть специальные функции get_image_front и get_image_bottom, данные функции НЕ работают на аппарате. Для получения видеоизображений воспользуйтесь функциями из OpenCV . Так же важно отметить, формат некоторых данных различен (показания курса, глубины). Имейте это ввиду.

Программирование - инициализация

Первым делом вам необходимо подключить библиотеку pymurapi. Для того, чтобы это сделать вызовите следующий код:

В дальнейшем mur будет использоваться в качестве алиаса на библиотеку pymurapi.

Далее вам необходимо создать экземпляр объекта MurApiBase. Для этого вызовите функцию mur_init():

Важно: Функция mur_init должна быть вызвана только один раз! Множественные вызовы данной функции ведут к неопределенному поведению.

Все готово! Библиотека pymurapi подключена и инициализирована. Вся дальнейшая работа с аппаратом или симулятором будет происходить через объект auv.

Список функций и их описание

Важно: В симуляторе моторы 0 и 1 отвечают за движение вперед/назад. Мотор 2 и 3 за всплытие/погружение. Мотор 4 за движение влево/вправо.

Функции, доступные ТОЛЬКО в симуляторе:

Важно: Функции, приведенные ниже, доступны только в симуляторе, на аппарате данный код работать НЕ будет. Мы вас предупредили.

 

 

 

- - \ No newline at end of file + + +

Hello, mur!

+

Описание интерфейса

+

Интерфейс MUR IDE состоит из 10 основных элементов:

+ + +

Подключение к аппарату

+

Все просто - вам нужно включить аппарат, подождать пока появится WiFi (mur_ssid), подключиться к нему (пароль по умолчанию vladivostok) и... Все готово!

+

Запись изображений с аппарата

+

В информационной вкладке Remote [10] отображается видео с камер аппарата. Видео можно записать (кнопка Record), а так же сделать фото (кнопка Capture). Сделанные фото и видео сохраняются на диске, куда установлена программа: C:\Users\User_name\Pictures\murImages

+

Если у вас возникли проблемы с получением изображения с аппарата, то Вам необходимо отключить Брандмауэр Windows.

+

Телеуправление с клавиатуры

+

Для телеуправления аппаратом с помощью клавиатуры необходимо нажать на информационную вкладку Remote [10], раскрыть меню Gamepad axes и нажать на появившуюся область.

+

Теперь вы можете управлять аппаратом с помощью кнопок W, A, S, D, Q, E.

+

Запуск симулятора

+

Запустить симулятор еще проще - достаточно нажать кнопку "Запуск симулятора" и подождать пока он откроется. Симулятор программируется точно так же как и аппарат, за исключением пары функций из pymurapi.

+
+

Важно: вы не можете управлять LED лентой в симуляторе. Для получения изображений из симулятора есть специальные функции get_image_front и get_image_bottom, данные функции НЕ работают на аппарате. Для получения видеоизображений воспользуйтесь функциями из OpenCV. Так же важно отметить, формат некоторых данных различен (показания курса, глубины). Имейте это ввиду.

+
+

Программирование - инициализация

+

Первым делом вам необходимо подключить библиотеку pymurapi. Для того, чтобы это сделать вызовите следующий код:

+
import pymurapi as mur
+
+

В дальнейшем mur будет использоваться в качестве алиаса на библиотеку pymurapi.

+

Далее вам необходимо создать экземпляр объекта MurApiBase. Для этого вызовите функцию mur_init():

+
auv = mur.mur_init()
+
+
+

Важно: Функция mur_init должна вызываться только один раз! Множественные вызовы данной функции ведут к неопределенному поведению.

+
+

Все готово! Библиотека pymurapi подключена и инициализирована. Вся дальнейшая работа с аппаратом или симулятором будет происходить через объект auv.

+

Список функций и их описание

+
def set_on_delay(value)
+# C помощью данной функции вы можете задать время, которое будет гореть LED лента на аппарате. 
+# Данная функция работает только на аппарате. 
+# Время задается в секундах.
+# Пример использования:
+auv.set_on_delay(0.5) 
+
+
def set_off_delay(value)
+# C помощью данной функции вы можете задать время, которое LED лента гореть не будет. 
+# Данная функция работает только на аппарате. 
+# Время задается в секундах.
+# Пример использования:
+auv.set_off_delay(1.8) 
+
+
def set_rgb_color(r, g, b):
+# C помощью данной функции вы можете задать цвет LED ленты в цветовом пространстве RGB.
+# Данная функция работает только на аппарате. 
+# Пример использования:
+auv.set_rgb_color(128, 0, 64) 
+
+
def set_single_led_color(led, r, g, b):
+# C помощью данной функции вы можете задать цвет отдельных светодиодов на LED ленте в цветовом пространстве RGB.
+# Данная функция работает только на аппаратах MiddleAUV выпущенных мая 2021 года и позже.
+# На LED ленте 13 светодиодов. 0 - это номер первого светодиода, 12 - последнего. 
+# Пример использования:
+auv.set_single_led_color(12, 128, 0, 64) 
+
+
def set_motor_power(motor_id, power)
+# C помощью данной функции вы можете задать тягу на моторы от -100 до 100. Где motor_id - номер мотора от 0 до 5 для аппарата и от 0 до 3 для симулятора. 
+# Пример использования:
+auv.set_motor_power(0, 50) 
+auv.set_motor_power(1, -50) 
+
+
+

Важно: В симуляторе моторы 0 и 1 отвечают за движение вперед/назад. Мотор 2 и 3 за всплытие/погружение. Мотор 4 за движение влево/вправо.

+
+
def get_depth()
+# Данная функция возвращает значение глубины. 
+# Пример использования:
+depth = auv.get_depth()
+
+
def get_yaw()
+# Данная функция возвращает значение курса. 
+# Пример использования:
+yaw = auv.get_yaw()
+
+
def get_pitch()
+# Данная функция возвращает значение крена. 
+# Пример использования:
+pitch = auv.get_pitch()
+
+
def get_roll()
+# Данная функция возвращает значение дифферента. 
+# Пример использования:
+roll = auv.get_roll()
+
+

Функции, доступные ТОЛЬКО в симуляторе:

+
+

Важно: Функции, приведенные ниже, доступны только в симуляторе, на аппарате данный код работать НЕ будет. Мы вас предупредили.

+
+
def get_image_front()
+# Даная функция возвращает изображение с передней камеры - тип cv2.Mat (BGR, CV8_UC3).
+# Данная функция работает только в симуляторе. 
+# Пример использования:
+image = auv.get_image_front()
+
+
def get_image_bottom()
+# Даная функция возвращает изображение с донной камеры - тип cv2.Mat (BGR, CV8_UC3).
+# Данная функция работает только в симуляторе. 
+# Пример использования:
+image = auv.get_image_bottom()
+
+
def drop()
+# Вызов данной функции приведет к сбросу сферического объекта в симуляторе.
+auv.drop()
+
+
def shoot()
+# Вызов данной функции приведет к выстрелу цилиндрическим объектом в симуляторе.
+auv.shoot()
+
+
def open_grabber()
+# Вызов данной функции приведет к раскрытию захвата в симуляторе.
+auv.open_grabber()
+
+
def close_grabber()
+# Вызов данной функции приведет к закрытию захвата в симуляторе.
+# Если в момент вызова захвата находился в контакте с объектом, то объект будет "захвачен".
+auv.close_grabber()
+
+
def get_hydrophone_signal():
+# Данная функция возвращает сигналы с трех гидрофонов и расстояние каждого из них до гидроакустического маяка на сцене.
+# Гидрофоны располагаются на переднем правом, переднем левом и на заднем правом моторе. В таком же порядке данная функция и возвращает значения.
+# Каждый из гидрофонов передает информацию о том, какой маяк транслирует сигнал. Номер маяка соответствует цифре сигнала (от 1 до 5). 
+# Всего может быть до пяти маяков на одной сцене.
+# В настройках симулятора доступны изменения некоторых параметров транслируемого сигнала. Pulse period - период импульса, pulse width - ширина импульса, spreading speed - скорость распространения сигнала. 
+tr, tl, fr, dist_tr, dist_tl, dist_fr = auv.get_hydrophone_signal()
+
+ + + + + diff --git a/resources/help/help.md b/resources/help/help.md index dd0b3cd..1f6f7aa 100644 --- a/resources/help/help.md +++ b/resources/help/help.md @@ -1,26 +1,42 @@ ## Hello, mur! #### Описание интерфейса -Интерфейс murIDE состоит из 10 основных элементов: - -- Кнопка переключения режимов [1] - локальное выполнение кода или выполнение кода на аппарате/режим телеуправления. При активном соединении с аппаратом иконка с ракетой будет зеленой, в противном случаи красной. -- Кнопка запуска скрипта [2]. При нажатии на данную кнопку будет запущен текущий скрипт. Если выбран режим Local - скрипт запускается локально на вашем компьютере. Если выбран режим Robot то скрипт будет запущен удаленно на аппарате. Запуск скрипта отменяет режим телеуправления. -- Кнопка остановки скрипта [3]. При нажатии на данную кнопку будет остановлен запущенный скрипт. Если выбран режим Local - будет остановлен скрипт, запущенный на вашем компьютере. Если выбран режим Robot то остановится скрипт, запущенный удаленно. -- Новый документ [4]. При нажатие на данную кнопку редактор кода будет очищен, изменения не сохраняться и будет создан виртуальный новый файл. -- Сохранить текущий документ [5]. При нажатие на данную кнопку текущий скрипт будет сохранен. Если вы сохраняете виртуальный файл, то появится диалоговое окно с предложением выбрать место куда ваш файл должен быть сохранен. -- Открыть документ [6]. При нажатие на данную появится диалоговое окно с предложением выбрать какой файл с кодом открыть. -- Запуск режима телеуправления [7]. Вы можете запустить данный режим только перейдя в режим Robot[1] и подключившись к аппарату. При активации данного режима во вкладке Remote [10] будут отображаться видеоизображения с камер аппарата, и вы сможете управлять аппаратом при помощи геймпада. -- Запуск симулятора [8]. При нажатие на данную кнопку откроется окно с симулятором. -- Включить/Выключить отображение телеметрии [9]. При нажатие на данную кнопку появится всплывающее окно с информацией о заряде батареи и показания с датчиков аппарата. Данная кнопка активна только при подключении к аппарату. -- Информационные вкладки [10]: Console/Messages N - отображение системных сообщений о запуске/остановке программ, так же в это окно попадают сообщения из stderr и stdout скрипта, запущенного на аппарате при активном подключение. Help - окно с данным текстом. Remote - окно в котором будут отображаться видеоизображения в режиме телеуправления [7]. + +Интерфейс MUR IDE состоит из 10 основных элементов: +![](qrc:/images/mur_ide_elements_new.png) + +* Кнопка переключения режимов [1] - локальное выполнение кода или выполнение кода на аппарате/режим телеуправления. При активном соединении с аппаратом иконка с ракетой будет зеленой, в противном случае красной. +* Кнопка запуска скрипта [2]. При нажатии на данную кнопку будет запущен текущий скрипт. Если выбран режим Local - скрипт запускается локально на вашем компьютере. Если выбран режим Robot то скрипт будет запущен удаленно на аппарате. Запуск скрипта отменяет режим телеуправления. +* Кнопка остановки скрипта [3]. При нажатии на данную кнопку будет остановлен запущенный скрипт. Если выбран режим Local - будет остановлен скрипт, запущенный на вашем компьютере. Если выбран режим Robot то остановится скрипт, запущенный удаленно. +* Новый документ [4]. При нажатие на данную кнопку редактор кода будет очищен, изменения не сохранятся и будет создан виртуальный новый файл. +* Сохранить текущий документ [5]. При нажатие на данную кнопку текущий скрипт будет сохранен. Если вы сохраняете виртуальный файл, то появится диалоговое окно с предложением выбрать место куда ваш файл должен быть сохранен. +* Открыть документ [6]. При нажатие на данную кнопку появится диалоговое окно с предложением выбрать какой файл с кодом открыть. +* Запуск режима телеуправления [7]. Вы можете запустить данный режим только перейдя в режим Robot [1] и подключившись к аппарату. При активации данного режима во вкладке Remote [10] будут отображаться видеоизображения с камер аппарата, и вы сможете управлять аппаратом при помощи геймпада. +* Запуск симулятора [8]. При нажатие на данную кнопку откроется окно с симулятором. +* Включить/Выключить отображение телеметрии [9]. При нажатие на данную кнопку появится всплывающее окно с информацией о заряде батареи и показания с датчиков аппарата. Данная кнопка активна только при подключении к аппарату. +* Информационные вкладки [10]. Console - отображение системных сообщений о запуске/остановке программ, так же в это окно попадают сообщения из stderr и stdout скрипта, запущенного на аппарате при активном подключение. Help - окно с данным текстом. Remote - окно в котором будут отображаться видеоизображения в режиме телеуправления [7]. #### Подключение к аппарату -Все просто - вам нужно включить аппарат, подождать пока появится WiFi (murAP), подключится к нему (пароль по умолчанию vladivostok) и... Все готово! + +Все просто - вам нужно включить аппарат, подождать пока появится WiFi (mur_ssid), подключиться к нему (пароль по умолчанию vladivostok) и... Все готово! + +#### Запись изображений с аппарата + +В информационной вкладке Remote [10] отображается видео с камер аппарата. Видео можно записать (кнопка Record), а так же сделать фото (кнопка Capture). Сделанные фото и видео сохраняются на диске, куда установлена программа: C:\Users\User_name\Pictures\murImages + +Если у вас возникли проблемы с получением изображения с аппарата, то Вам необходимо отключить Брандмауэр Windows. + +#### Телеуправление с клавиатуры + +Для телеуправления аппаратом с помощью клавиатуры необходимо нажать на информационную вкладку Remote [10], раскрыть меню Gamepad axes и нажать на появившуюся область. + +Теперь вы можете управлять аппаратом с помощью кнопок W, A, S, D, Q, E. #### Запуск симулятора + Запустить симулятор еще проще - достаточно нажать кнопку "Запуск симулятора" и подождать пока он откроется. Симулятор программируется точно так же как и аппарат, за исключением пары функций из pymurapi. -> **Важно**: вы не можете управлять LED лентой в симуляторе. Для получения изображений из симулятора есть специальные функции `get_image_front` и `get_image_bottom`, данные функции **НЕ** работают на аппарате. Для получения видеоизображений воспользуйтесь функциями из OpenCV . Так же важно отметить, формат некоторых данных различен (показания курса, глубины). Имейте это ввиду. +> **Важно**: вы не можете управлять LED лентой в симуляторе. Для получения изображений из симулятора есть специальные функции ***get_image_front*** и ***get_image_bottom***, данные функции **НЕ** работают на аппарате. Для получения видеоизображений воспользуйтесь функциями из OpenCV. Так же важно отметить, формат некоторых данных различен (показания курса, глубины). Имейте это ввиду. #### Программирование - инициализация @@ -34,11 +50,11 @@ import pymurapi as mur Далее вам необходимо создать экземпляр объекта MurApiBase. Для этого вызовите функцию mur_init(): -```python +```Python auv = mur.mur_init() ``` -> **Важно:** Функция mur_init должна быть вызвана только один раз! Множественные вызовы данной функции ведут к неопределенному поведению. +> **Важно:** Функция mur_init должна вызываться только один раз! Множественные вызовы данной функции ведут к неопределенному поведению. Все готово! Библиотека pymurapi подключена и инициализирована. Вся дальнейшая работа с аппаратом или симулятором будет происходить через объект auv. @@ -52,7 +68,7 @@ def set_on_delay(value) # Пример использования: auv.set_on_delay(0.5) ``` - + ```Python def set_off_delay(value) # C помощью данной функции вы можете задать время, которое LED лента гореть не будет. @@ -68,6 +84,15 @@ def set_rgb_color(r, g, b): # Данная функция работает только на аппарате. # Пример использования: auv.set_rgb_color(128, 0, 64) +``` + +```Python +def set_single_led_color(led, r, g, b): +# C помощью данной функции вы можете задать цвет отдельных светодиодов на LED ленте в цветовом пространстве RGB. +# Данная функция работает только на аппаратах MiddleAUV выпущенных мая 2021 года и позже. +# На LED ленте 13 светодиодов. 0 - это номер первого светодиода, 12 - последнего. +# Пример использования: +auv.set_single_led_color(12, 128, 0, 64) ``` ```Python @@ -77,8 +102,8 @@ def set_motor_power(motor_id, power) auv.set_motor_power(0, 50) auv.set_motor_power(1, -50) ``` - -> **Важно:** В симуляторе моторы 0 и 1 отвечают за движение вперед/назад. Мотор 2 и 3 за всплытие/погружение. Мотор 4 за движение влево/вправо. + +> **Важно:** В симуляторе моторы 0 и 1 отвечают за движение вперед/назад. Мотор 2 и 3 за всплытие/погружение. Мотор 4 за движение влево/вправо. ```Python def get_depth() @@ -92,27 +117,27 @@ def get_yaw() # Данная функция возвращает значение курса. # Пример использования: yaw = auv.get_yaw() -``` +``` ```Python def get_pitch() # Данная функция возвращает значение крена. # Пример использования: pitch = auv.get_pitch() -``` +``` ```Python def get_roll() -# Данная функция возвращает значение диффирента. +# Данная функция возвращает значение дифферента. # Пример использования: roll = auv.get_roll() -``` - +``` + #### Функции, доступные **ТОЛЬКО** в симуляторе: -> **Важно:** Функции, приведенные ниже, доступны только в симуляторе, на аппарате данный код работать **НЕ** будет. *Мы вас предупредили*. - -```Python +> **Важно:** Функции, приведенные ниже, доступны только в симуляторе, на аппарате данный код работать **НЕ** будет. _Мы вас предупредили_. +> +```Python def get_image_front() # Даная функция возвращает изображение с передней камеры - тип cv2.Mat (BGR, CV8_UC3). # Данная функция работает только в симуляторе. @@ -125,110 +150,41 @@ def get_image_bottom() # Даная функция возвращает изображение с донной камеры - тип cv2.Mat (BGR, CV8_UC3). # Данная функция работает только в симуляторе. # Пример использования: -image = auv.get_image_front() -``` +image = auv.get_image_bottom() +``` ```Python def drop() -# Вызов данный функции приведет к сбросу сферического объектов в симуляторе. +# Вызов данной функции приведет к сбросу сферического объекта в симуляторе. auv.drop() -``` +``` -```python +```Python def shoot() -# Вызов данный функции приведет к выстрелу целиндрическим объектов в симуляторе. +# Вызов данной функции приведет к выстрелу цилиндрическим объектом в симуляторе. auv.shoot() ``` ```Python def open_grabber() -# Вызов данный функции приведет к раскрытию захвата в симуляторе. +# Вызов данной функции приведет к раскрытию захвата в симуляторе. auv.open_grabber() -``` +``` ```Python def close_grabber() -# Вызов данный функции приведет к закрытию захвата в симуляторе. +# Вызов данной функции приведет к закрытию захвата в симуляторе. # Если в момент вызова захвата находился в контакте с объектом, то объект будет "захвачен". auv.close_grabber() -``` +``` ```Python -def get_pinger_data(pinger_id): -# Данная функция возвращает угол и расстояния до гидроакустического маяка на сцене. -# Всего может быть до четырех маяков в одной сцене. -# Аргументом данной функции является ID маяка (0, 1, 2 или 3). -# Если маяк с таким ID на сцене отсутствует - данная функция вернет 0, 0. -# Маяки имеют определенную частоту обновления. По умолчанию частота обновления значений установлена в 2000 миллисекунд. -# Значение частоты обновления может быть изменено в настройках симулятора. -angle, distance = auv.get_pinger_data(0) -angle, distance = auv.get_pinger_data(3) -``` - -### Работа с MiddleUSV +def get_hydrophone_signal(): +# Данная функция возвращает сигналы с трех гидрофонов и расстояние каждого из них до гидроакустического маяка на сцене. +# Гидрофоны располагаются на переднем правом, переднем левом и на заднем правом моторе. В таком же порядке данная функция и возвращает значения. +# Каждый из гидрофонов передает информацию о том, какой маяк транслирует сигнал. Номер маяка соответствует цифре сигнала (от 1 до 5). +# Всего может быть до пяти маяков на одной сцене. +# В настройках симулятора доступны изменения некоторых параметров транслируемого сигнала. Pulse period - период импульса, pulse width - ширина импульса, spreading speed - скорость распространения сигнала. +tr, tl, fr, dist_tr, dist_tl, dist_fr = auv.get_hydrophone_signal() +``` -API для работы с MiddleUSV очень похож, на API для работы с MiddleAUV. Ниже будут рассмотренны основные различия между ними. - - -#### Программирование - инициализация - -Первым делом вам необходимо подключить библиотеку pymurapi. Для того, чтобы это сделать вызовите следующий код: - -```Python -import pymurapi as mur -``` - -В дальнейшем mur будет использоваться в качестве алиаса на библиотеку pymurapi. - -Далее вам необходимо создать экземпляр объекта MurApiBase. Для этого вызовите функцию mur_init(): - -```python -usv = mur.usv_init() -``` - -#### Список функций и их описание специфичных для MiddleUSV - -#### Список функций специфичных для MiddleUSV - -```python -def set_servo(angle): -# С помощью данной функции можно установить угол поворота серво-камеры -# аргумент angle - целое число от 0 до 180 -usv.set_servo(90) -``` -```python -def get_gps_satellites(self): -# Функция возвращает количество видимых спутников GPS -sat = usv.get_gps_satellites() -``` -```python -def get_gps_alt(self): -# Функция возвращает высоту по GPS -alt = usv.get_gps_alt() -``` -```python -def get_gps_lat(self): -# Функця возвращает широту по GPS -lat = usv.get_gps_lat() -``` -```python -def get_gps_lng(self): -# Функця возвращает долготу по GPS -lng = usv.get_gps_lng() -``` -```python -def get_gps_speed(self): -# Функця возвращает скорость в км\ч по GPS -kmph = usv.get_gps_speed() -``` -```python -def get_gps_yaw(self): -# Функця возвращает курс относительно севера по GPS -yaw = usv.get_gps_yaw() -``` - -#### Функции, **не доступные** для MiddleUSV: - -```python -def get_depth() -``` diff --git a/resources/images/mur_ide_elements.png b/resources/images/mur_ide_elements.png deleted file mode 100644 index e83194b..0000000 Binary files a/resources/images/mur_ide_elements.png and /dev/null differ diff --git a/resources/images/mur_ide_elements_new.jpg b/resources/images/mur_ide_elements_new.jpg new file mode 100644 index 0000000..f700152 Binary files /dev/null and b/resources/images/mur_ide_elements_new.jpg differ diff --git a/resources/images/video_placeholder.png b/resources/images/video_placeholder.png index 4bf1768..ad716d3 100644 Binary files a/resources/images/video_placeholder.png and b/resources/images/video_placeholder.png differ diff --git a/resources/qml/Ui/AboutPopup.qml b/resources/qml/Ui/AboutPopup.qml index 23a819b..6fc23b8 100644 --- a/resources/qml/Ui/AboutPopup.qml +++ b/resources/qml/Ui/AboutPopup.qml @@ -1,35 +1,46 @@ import QtQuick 2.11 import QtQuick.Controls 2.12 import QtQml 2.2 -import QtQuick.Controls.Styles 1.4 Popup { id: aboutPopup; x: (parent.width - width) / 2 y: (parent.height - height) / 2 + bottomPadding: 24; closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside; modal: true; visible: true; - parent: ApplicationWindow.overlay; + parent: fullWindow; + Overlay.modal: Rectangle { + color: Style.overlayBack; + Behavior on opacity { + NumberAnimation { + duration: Style.animFastest + } + } + } background: Rectangle { anchors.fill: parent; - color: "#21252B"; + color: Style.bgDark; border.width: 1; - border.color: "#181A1F"; + border.color: Style.bgDarker; } Row { anchors.centerIn: parent - Image { - id: murLogo; - source: "qrc:/images/mur_logo256.png" - width: 64; - height: 64 - horizontalAlignment: Image.AlignLeft - verticalAlignment: Image.AlignTop + Column { + topPadding: 8; + Image { + id: murLogo; + source: "qrc:/images/mur_logo256.png" + width: 64; + height: 64 + horizontalAlignment: Image.AlignLeft + verticalAlignment: Image.AlignTop + } } Column { @@ -37,22 +48,54 @@ Popup { leftPadding: 8; Text { color: "#F39C12"; + font.underline: false; + linkColor: color; width: Math.min(220, contentWidth) - text: qsTr("The center for robotics development LLC.") + text: qsTr("The center for robotics development LLC.") wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter + 5; + onLinkActivated: Qt.openUrlExternally(link); } Text { anchors.topMargin: 8; anchors.leftMargin: 32; - - color: "#2B68A4"; width: Math.min(200, contentWidth) - text: qsTr("murIDE v0.0.8. Powered by: Qt, OpenCV, - GStreamer and Python"); + font.family: Style.fontSans; + color: Style.lighterGray; + linkColor: Style.lightBlue; + text: qsTr("

MUR IDE v" + Controllers.version + "


+ Source code + (MIT license) +

Powered by:
Qt, + OpenCV, + GStreamer + and Python +

+ Contact us: support@robocenter.org + "); wrapMode: Text.WordWrap onLinkActivated: Qt.openUrlExternally(link); } } } + + enter: Transition { + NumberAnimation { + property: "opacity"; + from: 0.0; + to: 1.0; + easing.type: Style.animEasing; + duration: Style.animDuration; + } + } + + exit: Transition { + NumberAnimation { + property: "opacity"; + from: 1.0; + to: 0.0; + easing.type: Style.animEasing; + duration: Style.animDuration; + } + } } diff --git a/resources/qml/Ui/Altimeter.qml b/resources/qml/Ui/Altimeter.qml new file mode 100644 index 0000000..a0fb9b4 --- /dev/null +++ b/resources/qml/Ui/Altimeter.qml @@ -0,0 +1,141 @@ +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import mur 1.0 + +Rectangle { + id: altimeter; + property real depth: Controllers.network.depth; + Behavior on depth { + NumberAnimation { + id: depthAnim; + alwaysRunToEnd: true; + duration: 200; + easing.type: Easing.Linear; + } + } + + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.verticalCenter: parent.verticalCenter; + width: 40; + height: Math.min(parent.height * 0.5, 300); + color: "transparent"; + border.width: 1; + border.color: "#70000000"; + + Item { + id: wrapper; + anchors.fill: parent; + clip: true; + + Rectangle { + id: meter; + property real parts: 8; + property real strokes: 4 * parts; + property real h: parent.height * 1.5; + anchors.verticalCenter: parent.verticalCenter; + height: h; + width: parent.width; + color: Style.outlineColor; + + Item { + anchors.fill: parent; + + Repeater { + model: meter.strokes; + + Rectangle { + width: 2 + parent.width / 4 * ((index + 3) % 2 * (index + 3) % 4); + anchors.horizontalCenter: parent.horizontalCenter; + y: (-altimeter.depth * 8 * meter.height / meter.strokes) % (meter.height / meter.parts * 2) + ((index + 4) * (meter.height / meter.strokes)); + + Rectangle { + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + width: parent.width; + height: 2; + color: "#FFFFFF"; + opacity: 0.15; + } + } + } + + Repeater { + model: meter.parts + 1; + + Rectangle { + y: (-altimeter.depth * 2 * meter.height / meter.parts) % (meter.height / meter.parts * 2) + ((index) * (meter.height / meter.parts)); + width: parent.width; + anchors.horizontalCenter: parent.horizontalCenter; + + Rectangle { + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + height: meter.height / 6; + width: parent.width; + opacity: index % 2 === 0 ? 0.4 : 0.1; + + gradient: Gradient { + orientation: Gradient.Vertical; + GradientStop { position: 0.0; color: "#00FFFFFF" } + GradientStop { position: 0.5; color: "#CCAAAAAA" } + GradientStop { position: 1.0; color: "#00FFFFFF" } + } + } + + UiLabel { + property bool big: index % 2 === 0; + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + property real offset: altimeter.depth >= 0 ? Math.floor(altimeter.depth) : Math.ceil(altimeter.depth); + property real val: (index - meter.parts / 2) / 2 + offset; + text: val; + font.pointSize: big ? 12 : 9; + font.bold: true; + color: "#FFFFFF"; + opacity: 0.5; + style: Text.Outline; + styleColor: Style.outlineColor; + } + } + } + } + } + } + + Icon { + anchors.right: altimeter.left; + anchors.verticalCenter: altimeter.verticalCenter; + icon: icons.fa_caret_right; + font.pointSize: 16; + color: Controllers.image.speedMode === 0 ? Style.gray : Controllers.image.speedMode === 2 ? Style.yellow : Style.white; + style: Text.Outline; + styleColor: Style.outlineColor; + } + + Rectangle { + anchors.verticalCenter: altimeter.verticalCenter; + anchors.left: altimeter.left; + anchors.right: altimeter.right; + height: 50; + + gradient: Gradient { + orientation: Gradient.Vertical; + GradientStop { position: 0.0; color: "#00000000" } + GradientStop { position: 0.5; color: "#BB000000" } + GradientStop { position: 1.0; color: "#00000000" } + } + } + + UiLabel { + anchors.left: altimeter.left; + anchors.verticalCenter: altimeter.verticalCenter; + anchors.horizontalCenter: altimeter.horizontalCenter; + horizontalAlignment: Text.AlignHCenter; + font.bold: true; + text: Controllers.network.depth.toFixed(1); + color: "white"; + style: Text.Outline; + styleColor: Style.outlineColor; + } +} diff --git a/resources/qml/Ui/ApplicationHeader.qml b/resources/qml/Ui/ApplicationHeader.qml index 04d4e23..95dda78 100644 --- a/resources/qml/Ui/ApplicationHeader.qml +++ b/resources/qml/Ui/ApplicationHeader.qml @@ -1,20 +1,25 @@ import QtQuick 2.9 - Rectangle { id: appHeader; height: visible ? 32 : 0; - color: "#21252B"; + color: Style.bgDark; property var controller: Controllers.menu; - signal remotePressed; Loader { id: popupLoader; active: false; - source: "qrc:/qml/Ui/TelimetryPopup.qml"; - anchors.fill: parent + source: "qrc:/qml/Ui/TelemetryPopup.qml"; + anchors.fill: parent; + } + + Loader { + id: batteryLoader; + active: false; + source: "qrc:/qml/Ui/BatterySettings.qml"; + anchors.fill: parent; } Rectangle { @@ -22,7 +27,7 @@ Rectangle { anchors.right: parent.right; anchors.bottom: parent.bottom; height: 1; - color: "#181A1F"; + color: Style.bgDarker; } Row { @@ -38,37 +43,52 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter; frameless: false; toolTip: "Run code remote/local"; + width: font.pixelSize * 7; label.text: Controllers.scripts.local ? "Local" : "Robot"; highlight: Controllers.scripts.local; icon: Controllers.scripts.local ? icons.fa_desktop : Controllers.network.usv ? icons.fa_ship : icons.fa_rocket; - iconColor: !Controllers.scripts.local ? Controllers.network.connected ? "#148F77" : "#E74C3C" : "#fff"; + iconColor: !Controllers.scripts.local ? Controllers.network.connected ? Style.green : Style.red : Style.white; onClicked: { controller.onTargetModeChanged(); + popupLoader.active = false; } } UiButton { - anchors.verticalCenter: parent.verticalCenter; + id: buttonStartScript; + property bool isRunning: Controllers.scripts.running || Controllers.network.running; + anchors.verticalCenter: parent.verticalCenter; frameless: false; - icon: icons.fa_play_circle; - toolTip: "Start programm"; + icon: isRunning ? icons.fa_refresh : icons.fa_play_circle; + toolTip: "Start programm [F5]"; enabled: Controllers.scripts.local ? !Controllers.scripts.running : Controllers.network.connected ? !Controllers.network.running : false; + shortcut.sequence: "F5"; + iconRotation: isRunning ? iconRotation : 0; onClicked: { controller.onFileSave(); controller.onCodeRun(); } + + SequentialAnimation on iconRotation { + id: rotation; + running: Controllers.scripts.running || Controllers.network.running; + loops: Animation.Infinite; + PropertyAnimation { from: 0; to: 360; duration: 3000; } + } } UiButton { + id: buttonStopScript; anchors.verticalCenter: parent.verticalCenter; frameless: false; icon: icons.fa_stop_circle; - toolTip: "Stop programm"; - enabled: Controllers.scripts.local ? Controllers.scripts.running : Controllers.network.connected ? Controllers.network.running : false; + toolTip: "Stop programm [F6]"; + enabled: Controllers.scripts.local ? Controllers.scripts.running : Controllers.network.connected ? Controllers.network.running : false; + shortcut.sequence: "F6"; onClicked: { controller.onCodeStop(); } @@ -87,7 +107,6 @@ Rectangle { UiButton { anchors.verticalCenter: parent.verticalCenter; - frameless: false; icon: icons.fa_folder_open; toolTip: "Open source file"; @@ -98,7 +117,6 @@ Rectangle { UiButton { anchors.verticalCenter: parent.verticalCenter; - frameless: false; icon: icons.fa_file; toolTip: "New source file"; @@ -108,6 +126,7 @@ Rectangle { } UiButton { + id: buttonStartRemote; anchors.verticalCenter: parent.verticalCenter; frameless: false; @@ -115,6 +134,7 @@ Rectangle { toolTip: "Start remote mode"; enabled: !Controllers.scripts.local && Controllers.network.connected && !Controllers.network.running; highlight: Controllers.network.remote && Controllers.network.connected; + shortcut.sequence: "F7"; onClicked: { controller.onRunRemote(); @@ -127,9 +147,76 @@ Rectangle { anchors.top: parent.top; anchors.bottom: parent.bottom; anchors.rightMargin: 4; - spacing: 4; + UiButton { + anchors.verticalCenter: parent.verticalCenter; + frameless: false; + visible: Controllers.devMode; + icon: icons.fa_wrench; + toolTip: "Developer options"; + onClicked: { + if(!batteryLoader.active) { + batteryLoader.active = true; + } + else { + batteryLoader.active = false; + } + } + } + + UiButton { + id: vehicle_button; + anchors.verticalCenter: parent.verticalCenter; + frameless: false; + toolTip: "Vehicle info"; + enabled: true; + visible: Controllers.network.connected && !Controllers.scripts.local; + label.text: Controllers.network.vehicle_name; + label.textFormat: Text.RichText; + iconColor: Style.batteryColor; + iconOpacity: 1.0; + + Icon { + visible: !Controllers.network.rov; + + parent: vehicle_button.after_item; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + verticalAlignment: Text.AlignVCenter; + font.pixelSize: 14; + icon: " " + icons.fa_bolt; + color: Style.yellow; + opacity: 0; + style: Text.Outline; + styleColor: "#4A4709"; + + SequentialAnimation on opacity { + running: Controllers.network.is_charging; + alwaysRunToEnd: true; + loops: Animation.Infinite; + PropertyAnimation { from: 0.0; to: 1.0; duration: 1000; easing.type: Easing.OutQuart; } + PropertyAnimation { from: 1.0; to: 0.0; duration: 1000; easing.type: Easing.OutQuart; } + } + } + + icon: { + if (Controllers.network.rov) { + icon: ""; + } else if (Controllers.network.battery >= 90) { + icon: icons.fa_battery_4; + } else if (Controllers.network.battery >= 70) { + icon: icons.fa_battery_3; + } else if (Controllers.network.battery >= 50) { + icon: icons.fa_battery_2; + } else if (Controllers.network.battery >= 20) { + icon: icons.fa_battery_1; + } else { + icon: icons.fa_battery_0; + } + } + } + UiButton { anchors.verticalCenter: parent.verticalCenter; frameless: false; @@ -141,11 +228,11 @@ Rectangle { } } - UiButton { anchors.verticalCenter: parent.verticalCenter; frameless: false; - toolTip: "Telimetry"; + toolTip: "Telemetry"; + shortcut.sequence: "F1"; icon: icons.fa_info_circle; highlight: popupLoader.active; diff --git a/resources/qml/Ui/ApplicationLogger.qml b/resources/qml/Ui/ApplicationLogger.qml index 60d14c9..1d6af47 100644 --- a/resources/qml/Ui/ApplicationLogger.qml +++ b/resources/qml/Ui/ApplicationLogger.qml @@ -6,71 +6,105 @@ import QtQml 2.2 Rectangle { id: root; - color: "#282C34"; + color: Style.bgBlue; + property string tabTitle: "Console"; - property string tabTitle: Controllers.logger.entries.length > 0 ? "Messages " + Controllers.logger.entries.length + "" : "Console" - - ListView { - id: listView; + ScrollView { + id: logScrollView; anchors.left: parent.left; anchors.right: parent.right; anchors.top: parent.top; anchors.bottom: footer.top; - ScrollBar.vertical: ScrollBar { - minimumSize: 0.2; - } - ScrollBar.horizontal: ScrollBar { - minimumSize: 0.2; - } - boundsBehavior: Flickable.StopAtBounds; + rightPadding: 2; + bottomPadding: 8; + clip: true; + ScrollBar.horizontal.policy: buttonWordWrap.active ? ScrollBar.AlwaysOff : ScrollBar.AsNeeded; - model: Controllers.logger.entries; - delegate: Label { - anchors.leftMargin: 4; - anchors.left: parent.left; - anchors.right: parent.right; - color: "#F39C12"; - text: modelData; + TextArea { + id: outputArea; + renderType: TextArea.NativeRendering; + topPadding: 2; + leftPadding: 4; + rightPadding: 0; + bottomPadding: 4; + readOnly: true; + color: Style.orange; + font.family: Style.fontMono; + font.pixelSize: 12; + textFormat: Text.RichText; + text: Controllers.logger.output; + wrapMode: buttonWordWrap.active ? Text.WordWrap : Text.NoWrap; + width: buttonWordWrap.active ? root.width : parent.width; + selectByMouse: true; + mouseSelectionMode: TextArea.SelectCharacters; + selectionColor: Style.semiDark; + selectedTextColor: Style.lighterGray; + + onTextChanged: { + if (outputArea.height > logScrollView.height) { + outputArea.cursorPosition = outputArea.length-1; + } + } } - onCountChanged: { - listView.positionViewAtEnd() + } + + + UiLabel { + id: placeholder; + anchors.fill: parent; + text: "Console output..."; + font.pixelSize: 18; + verticalAlignment: Text.AlignVCenter; + horizontalAlignment: Text.AlignHCenter; + textFormat: Text.RichText; + opacity: outputArea.length > 0 || parent.width < 200 ? 0 : 0.5; + clip: true; + + Behavior on opacity { + NumberAnimation { + duration: Style.animFast; + } } } Rectangle { id: footer; - + height: 21; anchors.left: parent.left; anchors.right: parent.right; anchors.bottom: parent.bottom; + color: Style.bgDark; + property bool shrink: width < 200; - height: footerCol.height; - - color: "#282C34"; - - Column { - id: footerCol; - - anchors.left: parent.left; + Row { anchors.right: parent.right; + anchors.top: parent.top; anchors.bottom: parent.bottom; + anchors.rightMargin: 4; + spacing: 4; + visible: footer.width > 80; - Rectangle { - anchors.left: parent.left; - anchors.right: parent.right; - height: 1; - - color: "#21252B"; + UiButton { + id: buttonWordWrap; + property bool active: false; + anchors.verticalCenter: parent.verticalCenter; + icon: active ? icons.fa_indent : icons.fa_align_justify; + label.text: footer.shrink ? "" : "Word wrap"; + highlight: active; + onClicked: { + active = !active; + logScrollView.ScrollBar.horizontal.position = 0; + } } UiButton { - anchors.right: parent.right; - anchors.rightMargin: 4; + id: buttonClearOutput; icon: icons.fa_trash_o; - label.text: "Clear" - visible: parent.width > 70; + label.text: footer.shrink ? "" : "Clear"; + anchors.verticalCenter: parent.verticalCenter; onClicked: Controllers.logger.clear(); } } } + } diff --git a/resources/qml/Ui/ApplicationMenu.qml b/resources/qml/Ui/ApplicationMenu.qml index d44c91e..78fe3fd 100644 --- a/resources/qml/Ui/ApplicationMenu.qml +++ b/resources/qml/Ui/ApplicationMenu.qml @@ -1,8 +1,7 @@ import QtQuick 2.9 import QtQuick.Controls 2.12 import QtQml 2.2 -import QtQuick.Controls.Styles 1.4 - +import QtQml.Models 2.12 MenuBar { id: menuBar; @@ -65,24 +64,24 @@ MenuBar { } ApplicationMenuItem { + title: qsTr("Settings"); + Loader { - id: gamepadPopupLoader; + id: settingsPopupLoader; active: false; - source: "qrc:/qml/Ui/GamepadSettings.qml"; + source: "qrc:/qml/Ui/SettingsPanel.qml"; } - title: qsTr("Settings"); + Action { - enabled: Controllers.image.Gamepad.Gamepad.connected; - text: qsTr("Gamepad"); + text: qsTr("IDE settings"); onTriggered: { - if(gamepadPopupLoader.active) { - gamepadPopupLoader.item.open(); - } - else { - gamepadPopupLoader.active = true; - } + if (settingsPopupLoader.active) { + settingsPopupLoader.item.open(); + } else { + settingsPopupLoader.active = true; } } + } } ApplicationMenuItem { @@ -140,9 +139,9 @@ MenuBar { contentItem: Text { text: exampleMenuItem.text; - font.family: "Segoe WPC"; + font.family: Style.fontSans; font.pointSize: 10; - color: exampleMenuItem.highlighted ? "#9DA5B4" : "#6E7582"; + color: exampleMenuItem.highlighted ? Style.lightGray : Style.gray; horizontalAlignment: Text.AlignLeft; verticalAlignment: Text.AlignVCenter; elide: Text.ElideRight; @@ -153,7 +152,7 @@ MenuBar { implicitWidth: 40; implicitHeight: 10; opacity: enabled ? 1 : 0.3; - color: exampleMenuItem.highlighted ? "#181A1F" : "transparent"; + color: exampleMenuItem.highlighted ? Style.bgDarker : "transparent"; } } } @@ -165,9 +164,9 @@ MenuBar { contentItem: Text { text: menuBarItem.text; - font.family: "Segoe WPC"; + font.family: Style.fontSans; font.pointSize: 10; - color: menuBarItem.highlighted ? "#9DA5B4" : "#6E7582"; + color: menuBarItem.highlighted ? "#9DA5B4" : Style.gray; horizontalAlignment: Text.AlignLeft; verticalAlignment: Text.AlignVCenter; elide: Text.ElideRight; @@ -178,17 +177,17 @@ MenuBar { implicitWidth: 40; implicitHeight: 8; opacity: enabled ? 1 : 0.3; - color: menuBarItem.highlighted ? "#181A1F" : "transparent"; + color: menuBarItem.highlighted ? Style.bgDarker : "transparent"; } } background: Rectangle { implicitWidth: 40; implicitHeight: 8; - color: "#21252B"; + color: Style.bgDark; Rectangle { - color: "#181A1F"; + color: Style.bgDarker; width: parent.width; height: 1; anchors.bottom: parent.bottom; diff --git a/resources/qml/Ui/ApplicationMenuItem.qml b/resources/qml/Ui/ApplicationMenuItem.qml index 84b9c12..2c52e43 100644 --- a/resources/qml/Ui/ApplicationMenuItem.qml +++ b/resources/qml/Ui/ApplicationMenuItem.qml @@ -1,7 +1,7 @@ import QtQuick 2.11 import QtQuick.Controls 2.12 import QtQml 2.2 -import QtQuick.Controls.Styles 1.4 + Menu { id: menu @@ -19,7 +19,7 @@ Menu { visible: menuItem.subMenu onPaint: { var ctx = getContext("2d") - ctx.fillStyle = menuItem.highlighted ? "#9DA5B4" : "#6E7582"; + ctx.fillStyle = menuItem.highlighted ? "#9DA5B4" : Style.gray; ctx.moveTo(15, 15) ctx.lineTo(width - 15, height / 2) ctx.lineTo(15, height - 15) @@ -28,36 +28,14 @@ Menu { } } - indicator: Item { - implicitWidth: 30 - implicitHeight: 30 - Rectangle { - width: 15 - height: 15 - anchors.centerIn: parent - visible: menuItem.checkable - border.color: "#181A1F" - color: "#6E7582"; - radius: 3 - Rectangle { - width: 9 - height: 9 - anchors.centerIn: parent - visible: menuItem.checked - color: "#181A1F" - radius: 2 - } - } - } - contentItem: Text { leftPadding: menuItem.indicator.width rightPadding: menuItem.arrow.width text: menuItem.text font.pointSize: 10; opacity: enabled ? 1.0 : 0.3 - font.family: "Segoe WPC"; - color: menuItem.highlighted ? "#9DA5B4" : "#6E7582"; + font.family: Style.fontSans; + color: menuItem.highlighted ? "#9DA5B4" : Style.gray; elide: Text.ElideRight; renderType: TextEdit.NativeRendering; horizontalAlignment: Text.AlignLeft @@ -68,20 +46,28 @@ Menu { implicitWidth: 20 implicitHeight: 8 opacity: enabled ? 1 : 0.3 - color: menuItem.highlighted ? "#181A1F" : "transparent"; + color: menuItem.highlighted ? Style.bgDarker : "transparent"; } } background: Rectangle { implicitWidth: 180; implicitHeight: 30; - color: "#21252B"; + color: Style.bgDark; Rectangle { - color: "#181A1F"; + color: Style.bgDarker; width: parent.width; height: 1; anchors.bottom: parent.bottom; } } + + PropertyAnimation on opacity { + running: visible; + from: 0.0; + to: 1.0; + duration: Style.animFast; + easing.type: Style.animEasing; + } } diff --git a/resources/qml/Ui/ApplicationStatusBar.qml b/resources/qml/Ui/ApplicationStatusBar.qml index 672c02a..27c737f 100644 --- a/resources/qml/Ui/ApplicationStatusBar.qml +++ b/resources/qml/Ui/ApplicationStatusBar.qml @@ -2,14 +2,14 @@ import QtQuick 2.6 Rectangle { height: 24; - color: "#21252B" + color: Style.bgDark Rectangle { anchors.left: parent.left; anchors.right: parent.right; anchors.top: parent.top; height: 1; - color: "#181A1F" + color: Style.bgDarker; } Row { @@ -29,8 +29,9 @@ Rectangle { UiLabel { anchors.verticalCenter: parent.verticalCenter; - text: Controllers.editor.fileUrl.length > 0 ? Controllers.editor.fileUrl : "Empty" + width: root.width * 0.7; + elide: Text.ElideLeft; font.pointSize: 10; } } @@ -44,22 +45,9 @@ Rectangle { anchors.rightMargin: 4; spacing: 4; - /* - Icon { - anchors.verticalCenter: parent.verticalCenter; - icon: Controllers.network.battery >= 70 ? icons.fa_battery_full : Controllers.network.battery >= 50 ? icons.fa_battery_half : Controllers.network.battery >= 20 ? icons.fa_battery_quarter : icons.fa_battery_empty; - color: Controllers.network.connected ? Controllers.network.battery < 20 ? "#E74C3C" : "#148F77" : "#626567"; - } - - Icon { - anchors.verticalCenter: parent.verticalCenter; - icon: icons.fa_circle; - color: Controllers.network.connected ? "#148F77" : "#E74C3C"; - } - */ UiLabel { anchors.verticalCenter: parent.verticalCenter; - text: "version: " + "0.0.8"; + text: "Version: " + Controllers.version; } } } diff --git a/resources/qml/Ui/ApplicationTextEdit.qml b/resources/qml/Ui/ApplicationTextEdit.qml index a3c633c..3400ba9 100644 --- a/resources/qml/Ui/ApplicationTextEdit.qml +++ b/resources/qml/Ui/ApplicationTextEdit.qml @@ -1,10 +1,10 @@ import QtQuick 2.11 import QtQuick.Controls 2.12 -import QtQuick.Controls.Styles 1.4 + TextEdit { renderType: TextEdit.NativeRendering; - font.family: "Consolas"; + font.family: Style.fontMono; font.pointSize: 10; font.bold: false; selectByMouse: true diff --git a/resources/qml/Ui/AutoCompliter.qml b/resources/qml/Ui/AutoCompliter.qml index 4d6e3ea..232fae9 100644 --- a/resources/qml/Ui/AutoCompliter.qml +++ b/resources/qml/Ui/AutoCompliter.qml @@ -15,7 +15,6 @@ Rectangle { implicitWidth: contentItem.childrenRect.width; implicitHeight: contentHeight; boundsBehavior: Flickable.StopAtBounds; - // text: modelData; model: Controllers.logger.entries; delegate: Rectangle { diff --git a/resources/qml/Ui/BatterySettings.qml b/resources/qml/Ui/BatterySettings.qml new file mode 100644 index 0000000..d2852b1 --- /dev/null +++ b/resources/qml/Ui/BatterySettings.qml @@ -0,0 +1,142 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.12 +import QtQml 2.2 +import QtQuick.Layouts 1.12 + +Popup { + id: batteryPopup; + x: (parent.width - width) / 2 + y: menuBar.y + width: parent.width / 1; + height: parent.height + menuBar.height; + closePolicy: Popup.CloseOnEscape; + modal: true; + visible: true; + parent: fullWindow; + + background: Rectangle { + id: back; + anchors.fill: parent; + color: Style.bgDark; + border.width: 1; + border.color: Style.bgDarker; + } + + Overlay.modal: Rectangle { + color: Style.overlayBack; + Behavior on opacity { + NumberAnimation { + duration: Style.animFastest + } + } + } + + ColumnLayout { + anchors.top: parent.top; + width: Math.min(parent.width, 900); + anchors.horizontalCenter: parent.horizontalCenter; + + Row { + Layout.alignment: Qt.AlignHCenter; + + UiLabel { + anchors.verticalCenter: parent.verticalCenter; + font.pointSize: Style.headerFontSize; + text: "Battery settings"; + } + } + + Rectangle { + Layout.fillWidth: true; + width: parent.width; + height: 1; + color: Style.bgBlue; + } + + GridLayout{ + id: grid; + columns: 2; + rowSpacing: 10; + columnSpacing: 15; + Layout.leftMargin: 35; + Layout.topMargin: 25; + + UiLabel{ + text: "Cycle count\n" + "Full charge capacity\n" + + "Max error\n" + "Remaining capacity\n" + + "Reset count\n" + "Update status"; + } + UiLabel{ + text: Controllers.network.fg_cycle_count + "\n" + + Controllers.network.fg_full_charge_capacity + "\n" + + Controllers.network.fg_max_error + "\n" + + Controllers.network.fg_remaining_capacity + "\n" + + Controllers.network.fg_reset_count + "\n" + + Controllers.network.fg_update_status; + } + UiLabel{ + text: "Temperature\n" + "Amperage\n" + "Voltage"; + } + UiLabel{ + text: Controllers.network.fg_temp.toFixed(2) + " °C\n" + + Controllers.network.amperage.toFixed(2) + " A\n" + + Controllers.network.voltage.toFixed(2) + " V"; + } + UiLabel{ + text: "Flags:\n" + "FC\n" + "QEN\n" + + "RUP_DIS\n" + "VOK\n" + "OCVTAKEN"; + } + + UiLabel{ + text: "\n" + Controllers.network.fg_flag_fc + "\n" + + Controllers.network.fg_flag_qen + "\n" + + Controllers.network.fg_flag_rup_dis + "\n" + + Controllers.network.fg_flag_vok_flag + "\n" + + Controllers.network.fg_flag_ocvtaken; + } + UiButton { + label.text: "RESET"; + width: 120; + enabled: resetConfirm.checked; + onClicked: { + Controllers.network.batteryButton("reset"); + } + } + UiCheckbox{ + id: resetConfirm; + label.text: "Are you sure?"; + checked: false; + } + UiButton { + label.text: "IT_ENABLE"; + width: 120; + enabled: itEnableConfirm.checked; + onClicked: { + Controllers.network.batteryButton("it_enable"); + } + } + UiCheckbox{ + id: itEnableConfirm; + label.text: "Are you sure?"; + checked: false; + } + } + } + + Row { + anchors.right: parent.right; + anchors.bottom: parent.bottom; + + UiButton { + id: batteryOkButton; + anchors.verticalCenter: parent.verticalCenter; + label.text: "OK"; + label.font.bold: true; + onClicked: { + batteryPopup.close(); + } + } + + } + +} diff --git a/resources/qml/Ui/CodeEditor.qml b/resources/qml/Ui/CodeEditor.qml index 2000c56..f236075 100644 --- a/resources/qml/Ui/CodeEditor.qml +++ b/resources/qml/Ui/CodeEditor.qml @@ -1,7 +1,6 @@ import QtQuick 2.11 import QtQuick.Controls 2.12 import QtQml 2.2 -import QtQuick.Controls.Styles 1.4 import Hints 1.0 Rectangle { @@ -11,14 +10,13 @@ Rectangle { clip: true; - color: "#181A1F" + color: Style.bgDarker function activate() { textEdit.forceActiveFocus(); } Flickable { - id: flickable; anchors.left: lines.right; @@ -30,10 +28,11 @@ Rectangle { contentHeight: textEdit.height; boundsBehavior: Flickable.StopAtBounds - ScrollBar.vertical: ScrollBar { } ScrollBar.horizontal: ScrollBar { } + maximumFlickVelocity : 1100.0; + function updateScrollX(x) { if (contentX >= x) { contentX = x; @@ -59,7 +58,7 @@ Rectangle { y: textEdit.cursorY; height: textEdit.cursorHeight; - color: "#21252B" + color: Style.bgDark } Repeater { @@ -91,7 +90,6 @@ Rectangle { property bool showCursor: true; Keys.onPressed: { - if ((event.key === Qt.Key_Z || event.key === Qt.Key_Y) && (Qt.ShiftModifier && Qt.ControlModifier || Qt.ControlModifier)) { @@ -114,37 +112,8 @@ Rectangle { editor.hints.applyHint(Hints.HINT_COMMENT); event.accepted = true; } - } -// Loader { -// id: autoCompleateLoader; -// active: false; -// source: "qrc:/qml/Ui/AutoCompliter.qml"; -// x: textEdit.cursorX; -// y: textEdit.cursorY + textEdit.cursorHeight; -// } - -// Shortcut { -// sequence: "Ctrl+Space"; -// onActivated: { -// if(!autoCompleateLoader.active) { -// autoCompleateLoader.active = true; -// autoCompleateLoader.forceActiveFocus(); -// } -// else { -// autoCompleateLoader.active = false; -// autoCompleateLoader.activeFocusOnTab = true; -// } -// } -// } - -// onTextChanged: { -// if (autoCompleateLoader.active) { -// autoCompleateLoader.active = false; -// } -// } - onActiveFocusChanged: { if (activeFocus) { activateCursor(); @@ -169,6 +138,17 @@ Rectangle { width: 1.5; color: "orange"; visible: textEdit.showCursor && !textEdit.multiSelection; + antialiasing: true; + smooth: true; + } + + MouseArea { + anchors.fill: parent; + enabled: !textEdit.activeFocus; + cursorShape: Qt.IBeamCursor; + onClicked: { + root.activate(); + } } function activateCursor() { @@ -183,7 +163,7 @@ Rectangle { Timer { id: cursorTimer; - interval: 535;// Controllers.settings.cursorFlashTime; + interval: 535; running: true; repeat: true; onTriggered: textEdit.showCursor = !textEdit.showCursor; @@ -199,7 +179,7 @@ Rectangle { anchors.top: parent.top; anchors.bottom: footer.top; - color: "#181A1F" + color: Style.bgDarker width: linesCol.width + 4; @@ -220,7 +200,7 @@ Rectangle { font.pointSize: editor.fontSize; verticalAlignment: Text.AlignVCenter; - color: index >= editor.selection.startLine && index <= editor.selection.endLine ? "#fff" : "#6E7582" + color: index >= editor.selection.startLine && index <= editor.selection.endLine ? Style.white : Style.gray text: index + 1; } } @@ -232,7 +212,7 @@ Rectangle { anchors.right: parent.right; width: 1; - color: "#21252B" + color: Style.bgDark } } @@ -246,14 +226,6 @@ Rectangle { refocus: textEdit; } - MouseArea { - anchors.fill: parent; - enabled: !textEdit.activeFocus; - onClicked: { - root.activate(); - } - } - CodeEditorFooter { id: footer; anchors.left: parent.left; @@ -263,12 +235,12 @@ Rectangle { Connections { target: editor; - onClear: textEdit.clear(); - } - - Connections { - target: editor; - onSelect: textEdit.select(start, end); + function onClear () { + textEdit.clear(); + } + function onSelect() { + textEdit.select(editor.search.startPosition, editor.search.endPosition); + } } Component.onCompleted: { diff --git a/resources/qml/Ui/CodeEditorError.qml b/resources/qml/Ui/CodeEditorError.qml index eb2f26a..cb24264 100644 --- a/resources/qml/Ui/CodeEditorError.qml +++ b/resources/qml/Ui/CodeEditorError.qml @@ -24,7 +24,7 @@ Rectangle { label.text: error.message; label.font.pointSize: Controllers.editor.fontSize - 2; - color: "#181A1F" + color: Style.bgDarker border.color: root.color; } } diff --git a/resources/qml/Ui/CodeEditorFooter.qml b/resources/qml/Ui/CodeEditorFooter.qml index 96ac6e0..4904b6e 100644 --- a/resources/qml/Ui/CodeEditorFooter.qml +++ b/resources/qml/Ui/CodeEditorFooter.qml @@ -5,7 +5,7 @@ Rectangle { height: 22; - color: "#21252B" + color: Style.bgDark UiLabel { anchors.verticalCenter: parent.verticalCenter; @@ -21,6 +21,7 @@ Rectangle { anchors.bottom: parent.bottom; anchors.right: parent.right; anchors.rightMargin: 8; + visible: parent.width > 80; UiButton { anchors.verticalCenter: parent.verticalCenter; @@ -49,6 +50,6 @@ Rectangle { anchors.top: parent.top; height: 1; - color: "#181A1F" + color: Style.bgDarker } } diff --git a/resources/qml/Ui/CodeEditorSearch.qml b/resources/qml/Ui/CodeEditorSearch.qml index b32ecb2..2962956 100644 --- a/resources/qml/Ui/CodeEditorSearch.qml +++ b/resources/qml/Ui/CodeEditorSearch.qml @@ -7,7 +7,7 @@ Rectangle { property var controller: Controllers.editor.search; height: column.height; - color: "#21252B" + color: Style.bgDark visible: controller.visible; @@ -41,7 +41,7 @@ Rectangle { anchors.left: parent.left; anchors.right: parent.right; height: 1; - color: "#181A1F"; + color: Style.bgDarker; } Column { @@ -231,8 +231,6 @@ Rectangle { label.text: "Replace All"; icon: icons.fa_refresh; - enabled: false; - onClicked: { controller.replaceAll(); replaceInput.forceActiveFocus(); diff --git a/resources/qml/Ui/CodeEditorSearchInput.qml b/resources/qml/Ui/CodeEditorSearchInput.qml index e790c20..11a9f41 100644 --- a/resources/qml/Ui/CodeEditorSearchInput.qml +++ b/resources/qml/Ui/CodeEditorSearchInput.qml @@ -8,7 +8,7 @@ TextInput { property alias radius: rect.radius; property alias background: rect.color; - property color borderDefault: "#181A1F" + property color borderDefault: Style.bgDarker property color borderActive: "#9DA5B4" leftPadding: 4; @@ -18,8 +18,8 @@ TextInput { selectedTextColor: "#000"; selectionColor: root.borderActive; - font.family: "Roboto"; - font.pointSize: 13; + font.family: Style.fontSans; + font.pointSize: 12; verticalAlignment: TextInput.AlignVCenter; activeFocusOnPress: true; diff --git a/resources/qml/Ui/Compass.qml b/resources/qml/Ui/Compass.qml new file mode 100644 index 0000000..5b7ab7c --- /dev/null +++ b/resources/qml/Ui/Compass.qml @@ -0,0 +1,149 @@ +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import mur 1.0 + + +Rectangle { + id: compass; + property real targetYaw: Controllers.image.targetYaw + 180; + property real rawYaw: Controllers.network.yaw + 180; + property real yaw: -(rawYaw - 180) + 90 - (360 / 1.5); + + Behavior on rawYaw { + RotationAnimation { + id: yawAnim; + alwaysRunToEnd: true; + duration: 200; + easing.type: Easing.Linear; + direction: RotationAnimation.Shortest; + } + } + + Behavior on targetYaw { + RotationAnimation { + id: targetYawAnim; + alwaysRunToEnd: true; + duration: 50; + easing.type: Easing.Linear; + direction: RotationAnimation.Shortest; + } + } + + anchors.top: parent.top; + anchors.topMargin: 60; + width: Math.min(parent.width, 1000); + anchors.horizontalCenter: parent.horizontalCenter; + height: 40; + clip: true; + color: "transparent"; + border.width: 1; + border.color: "#70000000"; + + Rectangle { + id: meter; + property real parts: 8; + property real strokes: 4 * 16; + property real w: parent.width * 1.5; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + width: parent.width; + color: Style.outlineColor; + + Item { + anchors.fill: parent; + + Repeater { + model: meter.strokes; + + Rectangle { + x: (((compass.yaw + 360) * (meter.w / 360)) + (index * meter.w / meter.strokes)) % meter.w; + height: 2 + parent.height / 4 * ((index + 3) % 2 * (index + 3) % 4); + anchors.verticalCenter: parent.verticalCenter; + + Rectangle { + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + width: 2; + height: parent.height; + color: "#FFFFFF"; + opacity: 0.4; + } + } + } + + Repeater { + model: meter.parts * 2; + + Rectangle { + x: (((compass.yaw + 360) * (meter.w / 360)) + ((index + 2) * meter.w / meter.parts)) % (meter.w * 2) - meter.w; + height: parent.height; + anchors.verticalCenter: parent.verticalCenter; + + Rectangle { + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + width: meter.width / 10; + height: parent.height; + opacity: index % 2 === 0 ? 0.6 : 0.3; + + gradient: Gradient { + orientation: Gradient.Horizontal; + GradientStop { position: 0.0; color: "#00FFFFFF" } + GradientStop { position: 0.5; color: "#CCAAAAAA" } + GradientStop { position: 1.0; color: "#00FFFFFF" } + } + } + + UiLabel { + property bool big: index % 2 === 0; + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + property int degree: (360 / meter.parts * (index) - 180); + text: degree <= -180 ? degree + 360 : degree > 180 ? degree - 360 : degree; + font.pointSize: big ? 12 : 9; + font.bold: true; + color: "#FFFFFF"; + opacity: big ? 0.8 : 0.4; + style: Text.Outline; + styleColor: Style.outlineColor; + } + } + } + } + + Icon { + x: (((compass.yaw + 450 + compass.targetYaw) * (meter.w / 360)) % meter.w) - (width / 2); + anchors.top: meter.top; + anchors.topMargin: -7; + visible: Controllers.image.autoYawAltmode; + icon: icons.fa_caret_down; + font.pointSize: 16; + color: "#BBAAFFAA"; + style: Text.Outline; + styleColor: Style.outlineColor; + } + } + + Icon { + anchors.bottom: compass.bottom; + anchors.bottomMargin: -5; + anchors.horizontalCenter: compass.horizontalCenter; + icon: icons.fa_caret_up; + font.pointSize: 16; + color: Controllers.image.speedMode === 0 ? Style.gray : Controllers.image.speedMode === 2 ? Style.yellow : Style.white; + style: Text.Outline; + styleColor: Style.outlineColor; + } + + UiLabel { + anchors.topMargin: -2; + anchors.top: compass.top; + anchors.bottom: compass.bottom; + anchors.horizontalCenter: compass.horizontalCenter; + font.bold: true; + text: Controllers.network.yaw.toFixed(0); + color: "white"; + style: Text.Outline; + styleColor: Style.outlineColor; + } +} diff --git a/resources/qml/Ui/GamepadSettings.qml b/resources/qml/Ui/GamepadSettings.qml index e656034..46a5d77 100644 --- a/resources/qml/Ui/GamepadSettings.qml +++ b/resources/qml/Ui/GamepadSettings.qml @@ -1,7 +1,6 @@ import QtQuick 2.11 import QtQuick.Controls 2.12 import QtQml 2.2 -import QtQuick.Controls.Styles 1.4 Popup { id: gamepadPopup; diff --git a/resources/qml/Ui/GamepadWidget.qml b/resources/qml/Ui/GamepadWidget.qml new file mode 100644 index 0000000..fbffa81 --- /dev/null +++ b/resources/qml/Ui/GamepadWidget.qml @@ -0,0 +1,141 @@ +import QtQml 2.2 +import QtQuick 2.11 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import mur.GamepadAxes 1.0 + +ColumnLayout { + id: gamepadWidget; + property real widgetWidth: 100; + property real widgetHeight: 100; + property bool showIndicator: true; + property bool showValues: true; + property bool opaqueWhenConnected: true; + + RowLayout { + Layout.alignment: Qt.AlignHCenter; + visible: gamepadWidget.showIndicator; + + Icon { + style: Text.Outline; + styleColor: Style.outlineColor; + Layout.alignment: Qt.AlignHCenter; + icon: icons.fa_gamepad; + color: Controllers.joystick.joystickConnected ? Style.green : Style.red; + } + + UiLabel { + Layout.alignment: Qt.AlignHCenter; + text: Controllers.joystick.joystickConnected ? "Connected" : "Not connected"; + } + + } + + + RowLayout { + Layout.alignment: Qt.AlignHCenter; + Layout.fillWidth: true; + opacity: Controllers.joystick.joystickConnected || !opaqueWhenConnected ? 1.0 : 0.3; + + Item { + property real divider: 200 / gamepadWidget.widgetWidth; + width: gamepadWidget.widgetWidth; + height: gamepadWidget.widgetHeight; + + Rectangle { + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + color: "transparent"; + border.width: 4; + border.color: "#40FFFFFF"; + radius: 999; + height: parent.height; + width: height; + } + + Rectangle { + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + color: "transparent"; + border.width: 4; + border.color: "#40FFFFFF"; + radius: 999; + height: parent.height / 5; + width: parent.width / 5; + } + + Rectangle { + anchors.verticalCenter: parent.verticalCenter; + anchors.verticalCenterOffset: Controllers.joystick.allAxes[GamepadAxes.AxisY] / parent.divider; + anchors.horizontalCenter: parent.horizontalCenter; + anchors.horizontalCenterOffset: Controllers.joystick.allAxes[GamepadAxes.AxisX] / parent.divider; + color: "white"; + radius: 999; + height: parent.height / 7; + width: parent.width / 7; + } + } + + Item { + property real divider: 200 / gamepadWidget.widgetWidth; + width: gamepadWidget.widgetWidth; + height: gamepadWidget.widgetHeight; + + Rectangle { + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + color: "transparent"; + border.width: 4; + border.color: "#40FFFFFF"; + radius: 999; + height: parent.height; + width: height; + } + + Rectangle { + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + color: "transparent"; + border.width: 4; + border.color: "#40FFFFFF"; + radius: 999; + height: parent.height / 5; + width: parent.width / 5; + } + + Rectangle { + property int axis: 0; + anchors.verticalCenter: parent.verticalCenter; + anchors.verticalCenterOffset: Controllers.joystick.allAxes[GamepadAxes.AxisZ] / parent.divider ; + anchors.horizontalCenter: parent.horizontalCenter; + anchors.horizontalCenterOffset: Controllers.joystick.allAxes[GamepadAxes.AxisW] / parent.divider; + color: "white"; + radius: 999; + height: parent.height / 7; + width: parent.width / 7; + } + } + } + + Row { + Layout.alignment: Qt.AlignHCenter; + visible: gamepadWidget.showValues; + + Repeater { + model: [ + Controllers.joystick.allAxes[GamepadAxes.AxisX], + Controllers.joystick.allAxes[GamepadAxes.AxisY], + Controllers.joystick.allAxes[GamepadAxes.AxisZ], + Controllers.joystick.allAxes[GamepadAxes.AxisW], + ] + + UiLabel { + width: 50; + horizontalAlignment: Text.AlignHCenter; + text: modelData; + font.family: Style.fontMono; + } + } + } +} diff --git a/resources/qml/Ui/Icon.qml b/resources/qml/Ui/Icon.qml index 416a28f..572c3cd 100644 --- a/resources/qml/Ui/Icon.qml +++ b/resources/qml/Ui/Icon.qml @@ -12,7 +12,15 @@ Text { font.family: "FontAwesome"; font.pointSize: 13; style: Text.Normal; - color: enabled ? "#9DA5B4" : "#6E7582" + color: enabled ? Style.lightGray : Style.darkGray; textFormat: Text.PlainText; - verticalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + horizontalAlignment: Text.AlignHCenter; + + Behavior on color { + ColorAnimation { + duration: Style.animFastest; + easing.type: Style.animEasing; + } + } } diff --git a/resources/qml/Ui/MainWindow.qml b/resources/qml/Ui/MainWindow.qml index 89e4a47..792c5b9 100644 --- a/resources/qml/Ui/MainWindow.qml +++ b/resources/qml/Ui/MainWindow.qml @@ -1,19 +1,22 @@ import QtQuick 2.11 import QtQuick.Controls 2.4 + import QtQml 2.2 import QtWebView 1.1 +import QtQuick.Layouts 1.12 ApplicationWindow { id: root; visible: true; width: 640; - height: 480; - color: "#282C34"; + height: 640; + color: Style.bgBlue; minimumWidth: 640; minimumHeight: 640; menuBar: ApplicationMenu { id: menu; + visible: !fullWindow.visible; } Loader { @@ -32,13 +35,6 @@ ApplicationWindow { anchors.right: parent.right; anchors.top: parent.top; } - /* - CodeEditor { - anchors.top: header.bottom; - anchors.left: parent.left; - anchors.bottom: parent.bottom; - anchors.right: parent.right; - } */ SplitView { id: spliter; @@ -46,19 +42,19 @@ ApplicationWindow { anchors.left: parent.left; anchors.right: parent.right; anchors.bottom: footer.top; + leftItem: CodeEditor { anchors.fill: parent; } - - rightItem : TabView { + rightItem: TabView { + id: tabView; anchors.fill: parent; ApplicationLogger { anchors.fill: parent } - WebView { property string tabTitle: "Help"; anchors.fill: parent @@ -67,15 +63,47 @@ ApplicationWindow { RemoteView { anchors.fill: parent - property string tabTitle: "Remote"; } } } + ApplicationStatusBar { id: footer; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + } + Notifications { + id: notifications; + } + + Item { + id: fullWindow; + visible: false; anchors.left: parent.left; anchors.right: parent.right; + anchors.top: parent.top; anchors.bottom: parent.bottom; + + ColumnLayout { + id: fullWindowContainer; + anchors.fill: parent; + } + + Item { + id: fullWindowTop; + anchors.fill: parent; + + Compass { + id: compass; + visible: Controllers.network.rov; + } + + Altimeter { + id: altimeter; + visible: Controllers.network.rov; + } + } } } diff --git a/resources/qml/Ui/NotificationPopup.qml b/resources/qml/Ui/NotificationPopup.qml new file mode 100644 index 0000000..50e6681 --- /dev/null +++ b/resources/qml/Ui/NotificationPopup.qml @@ -0,0 +1,162 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.12 +import QtQml 2.2 +import QtQuick.Layouts 1.12 + +Popup { + id: notificationPopup; + property string type: "info"; + property string headerText: ""; + property string messageText: ""; + property string customIcon: ""; + property bool mini: false; + property bool disappear: false; + property int delay: 5000; + focus: false; + + property color accentColor: { + if (type === "ok") { + accentColor: Style.green; + } else if (type === "info") { + accentColor: Style.blue; + } else if (type === "warn" || type === "warning") { + accentColor: Style.orangeDark; + } else if (type === "error") { + accentColor: Style.red; + } else { + accentColor: Style.semiDarker; + } + }; + + x: (parent.width - width) + 1; + y: spliter.y + spliter.height - height * 3.0; + + width: mini ? 42: 300; + padding: 12; + + parent: fullWindow; + visible: true; + + Timer { + interval: notificationPopup.delay; + running: notificationPopup.disappear; + repeat: false; + onTriggered: notificationPopup.close(); + } + + background: Rectangle { + id: back; + anchors.fill: parent; + color: Style.semiDarker; + border.width: 1; + border.color: Style.bgDarker; + + SequentialAnimation on border.color { + alwaysRunToEnd: true; + loops: 1; + PropertyAnimation { from: accentColor; to: back.border.color; duration: 2000; } + } + } + + enter: Transition { + NumberAnimation { + property: "x"; + from: x + width; + to: x; + easing.type: Style.animEasing; + duration: Style.animDuration; + } + NumberAnimation { + property: "opacity"; + from: 0.0; + to: opacity; + easing.type: Style.animEasing; + duration: Style.animDuration; + } + } + + exit: Transition { + NumberAnimation { + property: "x"; + from: x; + to: x + width; + easing.type: Style.animEasing; + duration: Style.animDuration; + } + NumberAnimation { + property: "opacity"; + from: opacity; + to: 0.0; + easing.type: Style.animEasing; + duration: Style.animDuration; + } + } + + Column { + id: container; + anchors.fill: parent; + + RowLayout { + spacing: 6; + anchors.left: parent.left; + anchors.right: parent.right; + + Icon { + icon: { + if (notificationPopup.customIcon !== "") { + icon: notificationPopup.customIcon; + } else { + if (notificationPopup.type === "ok") { + icon: icons.fa_check_circle; + } else if (notificationPopup.type === "info") { + icon: icons.fa_info_circle; + } else if (notificationPopup.type === "warn" || notificationPopup.type === "warning") { + icon: icons.fa_exclamation_triangle; + } else if (notificationPopup.type === "error") { + icon: icons.fa_exclamation_circle; + } else { + icon: icons.fa_info_circle; + } + } + } + font.pointSize: 12; + width: 16; + Layout.alignment: Qt.AlignVCenter; + } + + UiLabel { + id: header; + visible: !notificationPopup.mini; + font.pointSize: 14; + text: notificationPopup.headerText; + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter; + } + + UiButton { + id: closeButton; + visible: !notificationPopup.mini; + frameless: true; + Layout.alignment: Qt.AlignRight + icon: icons.fa_times; + toolTip: "Close"; + opacity: 0.7; + onClicked: { + notificationPopup.close(); + } + } + } + + Row { + leftPadding: 23; + + UiLabel { + visible: !notificationPopup.mini; + id: message; + textFormat: Text.RichText; + text: notificationPopup.messageText; + } + } + + } +} diff --git a/resources/qml/Ui/Notifications.qml b/resources/qml/Ui/Notifications.qml new file mode 100644 index 0000000..52396d4 --- /dev/null +++ b/resources/qml/Ui/Notifications.qml @@ -0,0 +1,125 @@ +import QtQuick 2.11 +import QtQml 2.12 + +Item { + + Loader { + id: notificationLoader; + active: false; + source: "qrc:/qml/Ui/NotificationPopup.qml"; + onLoaded: { + notificationLoader.active = true; + } + } + + function notify(type, headerText, messageText = "", icon = "", disappear = false, delay = 5000, mini = false) { + notificationLoader.setSource("qrc:/qml/Ui/NotificationPopup.qml", + {"type": type, + "headerText": headerText, + "messageText": messageText, + "customIcon": icon, + "disappear": disappear, + "delay": delay, + "mini": mini + }) + notificationLoader.active = true; + notificationLoader.item.open(); + } + + function notifyImageSaved() { + notify("info", "Image saved", "", fa.icons.fa_picture_o, true, 1500); + } + + function notifyEmptyImage() { + notify("info", "No camera image", "", "", true, 1500); + } + + Icon { + id: fa; + visible: false; + } + + Item { + id: last; + property bool connected: false; + property bool is_charging: false; + property bool running: false; + property bool remote: false; + } + + Connections { + target: Controllers.network; + + onConnectedChanged: function() { + if (last.connected === Controllers.network.connected || Controllers.network.connected === true) return; + + if (!Controllers.network.connected) { + notify("warn", "Disconnected", "Vehicle is unavailable", "", true); + } + + last.connected = Controllers.network.connected; + } + + onTelemetryUpdated: function() { + if (last.connected === Controllers.network.connected || Controllers.network.connected === false) return; + + if (Controllers.network.connected) { + notify("ok", "Connected", Controllers.network.vehicle_name, fa.icons.fa_rocket, true); + } + + if (Controllers.network.rov) { + spliter.collapseLeft(); + tabView.selectTab(2); + } + + last.connected = Controllers.network.connected; + } + + onIs_chargingChanged: function() { + if (last.is_charging === Controllers.network.is_charging) return; + + if (Controllers.network.is_charging) { + notify("ok", "Charging", "", fa.icons.fa_bolt, true); + } else { + notify("info", "Not charging", "", fa.icons.fa_plug, true); + } + + last.is_charging = Controllers.network.is_charging; + } + + onNotificationReceived: function(status, msg) { + if (msg === "run_when_charging") { + notify(status, "Motors disabled!", "Please, unplug charger before run"); + } + if (msg === "low_battery") { + notify(status, "Low battery level!", "", "", true, 2500); + } + if (msg === "already_running") { + notify(status, "Vehicle is busy", "Something is already running"); + } + } + } + + Connections { + target: Controllers.image.Joystick; + function onJoystickConnectedChanged(connected) { + if (connected) { + notify("ok", "Gamepad connected", "", fa.icons.fa_gamepad, true); + } else { + notify("warn", "Gamepad disconnected", "", fa.icons.fa_gamepad, true); + } + } + } + + Connections { + target: Controllers.image; + function onRecordingVideoChanged() { + if (Controllers.image.recordingVideo) { + notify("info", "Recording video", "", fa.icons.fa_circle, true, 1500); + } else { + notify("info", "Recording stopped", "", fa.icons.fa_stop, true, 1500); + } + } + } + +} diff --git a/resources/qml/Ui/RemoteCamera.qml b/resources/qml/Ui/RemoteCamera.qml new file mode 100644 index 0000000..5913458 --- /dev/null +++ b/resources/qml/Ui/RemoteCamera.qml @@ -0,0 +1,89 @@ +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import mur 1.0 + +ColumnLayout { + id: cam; + property alias image: cameraImage.image; + property bool haveImage: false; + property bool isFullscreen: false; + property var originalParent: parent; + property string subtitle: ""; + function saveImage() {} + + Item { + parent: isFullscreen ? fullWindowContainer : cam; + Layout.fillWidth: true; + Layout.fillHeight: true; + + ImageItem { + id: cameraImage; + anchors.fill: parent; + + height: root.height; + width: root.width; + } + + UiLabel { + id: placeholder; + anchors.fill: parent; + verticalAlignment: Text.AlignVCenter; + horizontalAlignment: Text.AlignHCenter; + text: "

Video

" + subtitle; + textFormat: Text.RichText; + opacity: width > 150 && !haveImage ? 1.0 : 0.0; + width: root.width; + clip: true; + + Behavior on opacity { + NumberAnimation { + duration: Style.animFast; + easing.type: Style.animEasing; + } + } + } + + MouseArea { + id: mouseArea; + anchors.fill: parent; + hoverEnabled: true; + onClicked: { + if (isFullscreen) return; + + if (haveImage) { + saveImage(); + notifications.notifyImageSaved(); + } else { + notifications.notifyEmptyImage(); + } + } + } + + UiButton { + id: buttonFulscreeenCamera; + icon: cam.isFullscreen ? icons.fa_compress : icons.fa_expand; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + onClicked: { + cam.isFullscreen = !cam.isFullscreen; + fullWindow.visible = cam.isFullscreen; + if (!cam.isFullscreen) remoteToolbar.collapsed = false; + root.visibility = cam.isFullscreen ? "FullScreen" : "Windowed"; + } + frameless: true; + outline: true; + highlight: cam.isFullscreen; + width: height; + height: 32; + opacity: ma.containsMouse ? 1.0 : mouseArea.containsMouse ? 0.5 : 0.0; + } + + Connections { + target: cameraImage; + onImageChanged: function () { + haveImage = true; + } + } + } +} + diff --git a/resources/qml/Ui/RemoteCameraView.qml b/resources/qml/Ui/RemoteCameraView.qml new file mode 100644 index 0000000..6ca479b --- /dev/null +++ b/resources/qml/Ui/RemoteCameraView.qml @@ -0,0 +1,164 @@ +import QtQuick 2.0 +import mur 1.0 + + +Rectangle { + id: remoteView; + property var controller: Controllers.image; + height: parent.height; + width: parent.width; + color: Style.bgBlue; + property bool shrink: width < 300; + + Rectangle { + + id: remoteHeader; + height: 0; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + color: Style.bgDark; + } + + Rectangle { + id: spearator; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: remoteHeader.bottom; + height: 1; + color: Style.bgDarker; + } + + Column { + id: imageView; + anchors.left: parent.left; + anchors.top: spearator.bottom; + anchors.right: parent.right; + anchors.bottom: remoteFooter.top; + + Rectangle { + id: front; + implicitHeight: Controllers.network.usv ? parent.height : parent.height / 2; + implicitWidth: parent.width; + + MouseArea { + anchors.fill: parent; + onClicked: { + controller.saveImageFront(); + } + } + + ImageItem { + id: frontImage; + height: front.height + width: front.width + image: controller.front; + } + } + + Rectangle { + id: bottom; + visible: !Controllers.network.usv; + implicitHeight: parent.height / 2; + implicitWidth: parent.width; + + MouseArea { + anchors.fill: parent; + onClicked: { + controller.saveImageBottom(); + } + } + + ImageItem { + id: bottomImage; + height: bottom.height; + width: bottom.width; + image: controller.bottom; + } + } + } + + Rectangle { + id: remoteFooter; + height: 22; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + color: Style.bgDark; + + Row { + id: fileNameRow; + + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.leftMargin: 8; + + spacing: 4; + + Icon { + anchors.verticalCenter: parent.verticalCenter; + icon: icons.fa_gamepad; + color: controller.Gamepad.Gamepad.connected ? Style.green : Style.red; + } + + UiLabel { + visible: !remoteView.shrink; + anchors.verticalCenter: parent.verticalCenter; + text: remoteView.width < 350 ? "Gamepad: " : "Gamepad axes: "; + font.pointSize: 10; + } + + UiLabel { + font.family: Style.fontMono; + anchors.verticalCenter: parent.verticalCenter; + property var axisX: controller.Gamepad.axisXValue; + property var axisY: controller.Gamepad.axisYValue; + property var axisZ: controller.Gamepad.axisZValue; + + text: axisX +":"+axisY +":"+axisZ; + font.pointSize: 10; + } + } + + Row { + anchors.right: parent.right; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.rightMargin: 4; + spacing: 4; + visible: remoteView.width > 140; + + UiButton { + anchors.verticalCenter: parent.verticalCenter; + highlight: controller.autoYaw; + label.text: remoteView.shrink ? "" : "Auto Yaw"; + toolTip: "Auto Yaw regulator"; + icon: remoteView.shrink ? icons.fa_compass : null; + onClicked: { + controller.autoYaw = !controller.autoYaw; + } + + } + + UiButton { + anchors.verticalCenter: parent.verticalCenter; + highlight: controller.autoDepth; + label.text: remoteView.shrink ? "" : "Auto Depth"; + toolTip: "Auto Depth regulator"; + icon: remoteView.shrink ? icons.fa_arrows_v : null; + onClicked: { + controller.autoDepth = !controller.autoDepth; + } + } + } + } + + Rectangle { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: remoteFooter.top; + height: 1; + color: Style.bgDarker; + } +} diff --git a/resources/qml/Ui/RemoteKeyboardControl.qml b/resources/qml/Ui/RemoteKeyboardControl.qml new file mode 100644 index 0000000..34a6e36 --- /dev/null +++ b/resources/qml/Ui/RemoteKeyboardControl.qml @@ -0,0 +1,167 @@ +import QtQuick 2.12 +import mur 1.0 +import QtQuick.Layouts 1.12 + +import mur.GamepadAxes 1.0 + +Rectangle { + property color activeColor: keyboardControl.activeFocus ? Style.greenDark : Style.bgDark; + + gradient: Gradient { + orientation: Gradient.Vertical; + GradientStop { position: 0.0; color: Style.bgDark } + GradientStop { position: 1.0; color: activeColor } + } + + height: 80; + Layout.fillWidth: true; + focus: true; + clip: true; + + property bool shrink: width < 120; + property bool active: keyboardControl.activeFocus; + + Behavior on activeColor { + ColorAnimation { + duration: Style.animDuration; + easing.type: Style.animEasing; + } + } + + Item { + id: keyboardControl; + focus: true; + + property var bindings: { + "w": [GamepadAxes.AxisY, -1], + "s": [GamepadAxes.AxisY, +1], + "a": [GamepadAxes.AxisX, -1], + "d": [GamepadAxes.AxisX, +1], + "q": [GamepadAxes.AxisZ, -1], + "e": [GamepadAxes.AxisZ, +1], + "z": [GamepadAxes.AxisW, -1], + "x": [GamepadAxes.AxisW, +1], + }; + + Component.onCompleted: function() { + var keys = { + "w": "ц", + "a": "ф", + "s": "ы", + "d": "в", + "q": "й", + "e": "у", + "z": "я", + "x": "ч", + }; + + for (var key in keys) { + bindings[keys[key]] = bindings[key]; + } + } + + Keys.onPressed: { + if (event.modifiers & Qt.CtrlModifier) { + Controllers.joystick.setForceAxisValue(GamepadAxes.SpeedFast, 100); + } + + if (event.modifiers & Qt.ShiftModifier) { + Controllers.joystick.setForceAxisValue(GamepadAxes.SpeedSlow, 100); + } + + let action = bindings[event.text]; + + if (action !== undefined) { + let power = action[1] * 40; + Controllers.joystick.addForceAxisValue(action[0], power); + } + } + + Keys.onReleased: { + let action = bindings[event.text]; + + if (!event.modifiers && !event.isAutoRepeat) { + Controllers.joystick.setForceAxisValue(GamepadAxes.SpeedFast, 0); + Controllers.joystick.setForceAxisValue(GamepadAxes.SpeedSlow, 0); + } + + if (action !== undefined && !event.isAutoRepeat) { + Controllers.joystick.setForceAxisValue(action[0], 0); + } + } + } + + Icon { + visible: parent.shrink; + icon: icons.fa_keyboard_o; + pointSize: 28; + style: Text.Outline; + styleColor: Style.outlineColor; + anchors.horizontalCenter: parent.horizontalCenter; + anchors.verticalCenter: parent.verticalCenter; + } + + UiLabel { + visible: !parent.shrink; + opacity: ma.containsMouse && !keyboardControl.activeFocus ? 1.0 : 0.0; + anchors.horizontalCenter: parent.horizontalCenter; + anchors.verticalCenter: parent.verticalCenter; + anchors.topMargin: 2; + anchors.bottomMargin: 2; + text: keyboardControl.activeFocus ? "" : "Click to control\nwith keyboard"; + horizontalAlignment: Text.AlignHCenter; + font.bold: true; + font.pointSize: 16; + color: Style.lighterGray; + style: Text.Outline; + styleColor: Style.outlineColor; + + Behavior on opacity { + NumberAnimation { + duration: Style.animDuration; + easing.type: Style.animEasing; + } + } + } + + GamepadWidget { + visible: !parent.shrink; + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + opacity: (keyboardControl.activeFocus || Controllers.joystick.joystickConnected) && !(!keyboardControl.activeFocus && ma.containsMouse) ? 1.0 : 0.5; + widgetHeight: 60; + widgetWidth: widgetHeight; + showIndicator: false; + showValues: false; + opaqueWhenConnected: false; + + Behavior on opacity { + NumberAnimation { + duration: Style.animDuration; + easing.type: Style.animEasing; + } + } + } + + Rectangle { + id: separator; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + height: 1; + color: Style.outlineColor; + } + + MouseArea { + id: ma; + anchors.fill: parent; + hoverEnabled: true; + onClicked: { + if (!keyboardControl.activeFocus) { + keyboardControl.forceActiveFocus(); + } else { + keyboardControl.focus = false; + } + } + } +} diff --git a/resources/qml/Ui/RemoteView.qml b/resources/qml/Ui/RemoteView.qml index c0d76c1..6cb99bd 100644 --- a/resources/qml/Ui/RemoteView.qml +++ b/resources/qml/Ui/RemoteView.qml @@ -1,190 +1,352 @@ -import QtQuick 2.0 +import QtQuick 2.12 import mur 1.0 +import QtQuick.Layouts 1.12 + +import mur.GamepadAxes 1.0 Rectangle { id: remoteView; property var controller: Controllers.image; + property string tabTitle: controller.recordingVideo ? "Remote (recording)" : "Remote"; height: parent.height; width: parent.width; - color: "#282C34"; + color: Style.bgBlue; + property bool shrink: Controllers.network.rov ? width < 450 : width < 300; Rectangle { - id: remoteHeader; height: 22; anchors.top: parent.top; anchors.left: parent.left; anchors.right: parent.right; - color: "#21252B"; - - -// Row { - -// anchors.left: parent.left; -// anchors.top: parent.top; -// anchors.right: parent.right; -// anchors.bottom: parent.bottom; -// anchors.leftMargin: 4; -// spacing: 4; - -// UiButton { -// anchors.verticalCenter: parent.verticalCenter; -// frameless: true; -// toolTip: "Run code remote/local"; -// // label.text: Controllers.scripts.local ? "Local" : "Remote"; -// // highlight: Controllers.scripts.local; -// label.text: "Start/Stop remote mode"; -// icon: icons.fa_desktop; -// onClicked: { -// controller.on_target_mode_changed(); -// } -// } - -// UiButton { -// anchors.verticalCenter: parent.verticalCenter; -// frameless: true; -// toolTip: "Run code remote/local"; -// // label.text: Controllers.scripts.local ? "Local" : "Remote"; -// // highlight: Controllers.scripts.local; -// label.text: "Start/Stop remote mode"; -// icon: icons.fa_desktop; -// onClicked: { -// controller.on_target_mode_changed(); -// } -// } -// } + color: Style.bgDark; + + Row { + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + anchors.leftMargin: 8; + anchors.rightMargin: 8; + spacing: 4; + + UiButton { + frameless: true; + anchors.verticalCenter: parent.verticalCenter; + icon: icons.fa_angle_down; + rotation: keyboardControl.visible ? 180 : 0; + highlight: keyboardControl.visible; + + onClicked: { + keyboardControl.visible = !keyboardControl.visible; + } + } + + Icon { + anchors.verticalCenter: parent.verticalCenter; + anchors.verticalCenterOffset: -1; + icon: keyboardControl.active ? icons.fa_keyboard_o : icons.fa_gamepad; + color: keyboardControl.active ? Style.white : Controllers.joystick.joystickConnected ? Style.green : Style.red; + } + + UiLabel { + visible: remoteView.width > 200; + anchors.verticalCenter: parent.verticalCenter; + text: remoteView.shrink ? "Axes:" : "Gamepad axes:"; + font.pointSize: 10; + } + } + + Row { + spacing: 4; + width: parent.width < 200 ? parent.width - 40 : Math.min(parent.width / 2, 200); + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 4; + opacity: parent.width > 140 ? 1.0 : 0.0; + + UiLabel { + width: parent.width / 4; + font.family: Style.fontMono; + anchors.verticalCenter: parent.verticalCenter; + text: Controllers.joystick.allAxes[GamepadAxes.AxisX]; + horizontalAlignment: Text.AlignRight; + font.pointSize: 10; + } + + UiLabel { + width: parent.width / 4; + font.family: Style.fontMono; + anchors.verticalCenter: parent.verticalCenter; + text: Controllers.joystick.allAxes[GamepadAxes.AxisY]; + horizontalAlignment: Text.AlignRight; + font.pointSize: 10; + } + + UiLabel { + width: parent.width / 4; + font.family: Style.fontMono; + anchors.verticalCenter: parent.verticalCenter; + text: Controllers.joystick.allAxes[GamepadAxes.AxisZ]; + horizontalAlignment: Text.AlignRight; + font.pointSize: 10; + } + } } Rectangle { - id: spearator; + id: separator; anchors.left: parent.left; anchors.right: parent.right; anchors.bottom: remoteHeader.bottom; height: 1; - color: "#181A1F"; + color: Style.bgDarker; + visible: !keyboardControl.visible; } - Column { - id: imageView; + ColumnLayout { + id: remoteMain; anchors.left: parent.left; - anchors.top: spearator.bottom; + anchors.top: separator.bottom; anchors.right: parent.right; anchors.bottom: remoteFooter.top; + spacing: 0; - Rectangle { - id: front; - implicitHeight: Controllers.network.usv ? parent.height : parent.height / 2; - implicitWidth: parent.width; + RemoteKeyboardControl { + id: keyboardControl; + visible: false; + } - MouseArea { - anchors.fill: parent; - onClicked: { + GridLayout { + id: imageView; + property bool isVertical: width / height < 4 / 3; + columns: isVertical ? 1 : 2; + rowSpacing: 0; + columnSpacing: 0; + Layout.alignment: Qt.AlignVCenter; + + RemoteCamera { + Layout.fillWidth: true; + Layout.fillHeight: true; + id: front; + image: controller.front; + subtitle: "Camera 0"; + function saveImage() { controller.saveImageFront(); } } - ImageItem { - id: frontImage; - height: front.height - width: front.width - image: controller.front; - } - } - - Rectangle { - id: bottom; - visible: !Controllers.network.usv; - implicitHeight: parent.height / 2; - implicitWidth: parent.width; - - MouseArea { - anchors.fill: parent; - onClicked: { + RemoteCamera { + id: bottom; + Layout.fillWidth: true; + Layout.fillHeight: true; + visible: !Controllers.network.usv && !Controllers.network.rov; + image: controller.bottom; + subtitle: "Camera 1"; + function saveImage() { controller.saveImageBottom(); } } - - ImageItem { - id: bottomImage; - height: bottom.height; - width: bottom.width; - image: controller.bottom; - } } } Rectangle { id: remoteFooter; - height: 22; + height: 32; anchors.left: parent.left; anchors.right: parent.right; anchors.bottom: parent.bottom; - color: "#21252B"; + color: Style.bgDark; + clip: true; - Row { - id: fileNameRow; + property bool autoButtonsVisible: Controllers.network.rov ? remoteView.width > 240 : remoteView.width > 160; - anchors.top: parent.top; + Row { + id: remoteToolbar; + property bool collapsed: false; + anchors.top: fullWindow.visible ? undefined : parent.top; anchors.bottom: parent.bottom; - anchors.left: parent.left; - anchors.leftMargin: 8; - + anchors.horizontalCenter: collapsed ? parent.left : parent.horizontalCenter; + leftPadding: collapsed ? 34 : 0; + anchors.rightMargin: 4; + height: 22; spacing: 4; - Icon { + parent: fullWindow.visible ? fullWindow : remoteFooter; + opacity: fullWindow.visible ? 0.6 : 1.0; + + UiButton { + id: buttonCapture; anchors.verticalCenter: parent.verticalCenter; - icon: icons.fa_gamepad; - color: controller.Gamepad.Gamepad.connected ? "#148F77" : "#E74C3C"; + label.text: remoteView.width < 420 ? "" : "Capture"; + toolTip: "Capture both images [F8]"; + shortcut.sequence: "F8"; + icon: icons.fa_camera; + visible: !remoteToolbar.collapsed; + + onClicked: { + if (!front.haveImage && !bottom.haveImage) { + notifications.notifyEmptyImage(); + } else { + front.saveImage(); + bottom.saveImage(); + notifications.notifyImageSaved(); + } + } } - UiLabel { + UiButton { + id: buttonRecord; anchors.verticalCenter: parent.verticalCenter; - text: "Gamepad axes: "; - font.pointSize: 10; + label.text: remoteView.width < 420 ? "" : "Record"; + toolTip: "Record video from cameras [F9]"; + shortcut.sequence: "F9"; + icon: controller.recordingVideo ? icons.fa_circle : icons.fa_video_camera; + iconColor: controller.recordingVideo ? Style.red : Style.lightGray; + visible: !remoteToolbar.collapsed; + + onClicked: { + if (!controller.recordingVideo && !front.haveImage && !bottom.haveImage) { + notifications.notifyEmptyImage(); + } else { + controller.recordingVideo = !controller.recordingVideo; + } + } + + SequentialAnimation on iconOpacity { + running: controller.recordingVideo; + alwaysRunToEnd: true; + loops: Animation.Infinite; + PropertyAnimation { from: 1.0; to: 0.0; duration: 150; } + PropertyAnimation { from: 0.0; to: 1.0; duration: 150; } + PropertyAnimation { to: 1.0; duration: 700; } + } } - UiLabel { + UiButton { + visible: fullWindow.visible && !remoteToolbar.collapsed && remoteFooter.autoButtonsVisible; anchors.verticalCenter: parent.verticalCenter; - property var axisX: controller.Gamepad.axisXValue; - property var axisY: controller.Gamepad.axisYValue; - property var axisZ: controller.Gamepad.axisZValue; - - text: axisX +":"+axisY +":"+axisZ; - font.pointSize: 10; + toolTip: "Toggle compass"; + label.text: "Compass"; + icon : icons.fa_compass; + highlight: compass.visible; + onClicked: { + compass.visible = !compass.visible; + } } + UiButton { + visible: fullWindow.visible && !remoteToolbar.collapsed && remoteFooter.autoButtonsVisible; + anchors.verticalCenter: parent.verticalCenter; + toolTip: "Toggle altimeter"; + label.text: "Altimeter"; + icon : icons.fa_arrows_v; + highlight: altimeter.visible; + onClicked: { + altimeter.visible = !altimeter.visible; + } + } - } + Rectangle { + anchors.verticalCenter: parent.verticalCenter; + width: 1; + height: 18; + color: Style.bgDarker; + visible: !remoteToolbar.collapsed && remoteFooter.autoButtonsVisible; + } - Row { - anchors.right: parent.right; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.rightMargin: 4; + UiButton { + anchors.verticalCenter: parent.verticalCenter; + highlight: controller.autoYawAltmode; + label.text: remoteView.width < 420 ? "A" : "Alt Yaw"; + toolTip: "Yaw regulator Alt mode"; + visible: !remoteToolbar.collapsed && Controllers.network.rov && remoteFooter.autoButtonsVisible; + onClicked: { + controller.autoYawAltmode = !controller.autoYawAltmode; + } + } - spacing: 4; UiButton { anchors.verticalCenter: parent.verticalCenter; - frameless: false; highlight: controller.autoYaw; - label.text: "Auto Yaw"; - visible: remoteView.width > 275; + label.text: remoteView.shrink ? "Y" : Controllers.network.rov ? "Yaw" : "Auto Yaw"; + toolTip: "Auto Yaw regulator"; + icon: Controllers.network.rov ? "" : icons.fa_compass; + visible: !remoteToolbar.collapsed && remoteFooter.autoButtonsVisible; onClicked: { controller.autoYaw = !controller.autoYaw; } + } + + UiButton { + anchors.verticalCenter: parent.verticalCenter; + highlight: controller.autoPitch; + label.text: remoteView.shrink ? "P" : "Pitch"; + toolTip: "Auto Pitch regulator"; + visible: !remoteToolbar.collapsed && Controllers.network.rov && remoteFooter.autoButtonsVisible; + onClicked: { + controller.autoPitch = !controller.autoPitch; + } + } + UiButton { + anchors.verticalCenter: parent.verticalCenter; + highlight: controller.autoRoll; + label.text: remoteView.shrink ? "R" : "Roll"; + toolTip: "Auto Roll regulator"; + visible: !remoteToolbar.collapsed && Controllers.network.rov && remoteFooter.autoButtonsVisible; + onClicked: { + controller.autoRoll = !controller.autoRoll; + } } UiButton { anchors.verticalCenter: parent.verticalCenter; - frameless: false; highlight: controller.autoDepth; - label.text: "Auto Depth"; - visible: remoteView.width > 275; + label.text: remoteView.shrink ? "D" : Controllers.network.rov ? "Depth" : "Auto Depth"; + toolTip: "Auto Depth regulator"; + icon: Controllers.network.rov ? "" : icons.fa_arrows_v; + visible: !remoteToolbar.collapsed && remoteFooter.autoButtonsVisible; onClicked: { controller.autoDepth = !controller.autoDepth; } + } + + UiButton { + id: regulatorsToggle; + anchors.verticalCenter: parent.verticalCenter; + property bool allOff: !(controller.autoYaw || controller.autoDepth || controller.autoRoll || controller.autoPitch); + highlight: !allOff; + icon: icons.fa_toggle_on; + rotation: allOff ? 180 : 0; + toolTip: "Enable/disable all regulators"; + visible: !remoteToolbar.collapsed && remoteFooter.autoButtonsVisible; + frameless: true; + onClicked: { + let state = allOff; + controller.autoYaw = state; + controller.autoDepth = state; + controller.autoRoll = state; + controller.autoPitch = state; + } + } + + Rectangle { + visible: fullWindow.visible && !remoteToolbar.collapsed; + anchors.verticalCenter: parent.verticalCenter; + width: 1; + height: 18; + color: Style.bgDarker; + } + UiButton { + visible: fullWindow.visible; + anchors.verticalCenter: parent.verticalCenter; + toolTip: "Toggle panel"; + icon : remoteToolbar.collapsed ? icons.fa_arrow_right : icons.fa_arrow_left; + onClicked: { + remoteToolbar.collapsed = !remoteToolbar.collapsed; + } } } } @@ -194,6 +356,6 @@ Rectangle { anchors.right: parent.right; anchors.bottom: remoteFooter.top; height: 1; - color: "#181A1F"; + color: Style.bgDarker; } } diff --git a/resources/qml/Ui/RovPanel.qml b/resources/qml/Ui/RovPanel.qml new file mode 100644 index 0000000..1f43571 --- /dev/null +++ b/resources/qml/Ui/RovPanel.qml @@ -0,0 +1,185 @@ +import QtQml 2.2 +import QtQuick 2.11 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +Rectangle { + id: rovPanel; + anchors.fill: parent; + color: Style.bgDark; + + ColumnLayout { + anchors.fill: parent; + anchors.margins: 16; + anchors.topMargin: 8; + spacing: 8; + Layout.fillWidth: true; + Layout.fillHeight: false; + Layout.alignment: Qt.AlignCenter; + + RowLayout { + Layout.fillWidth: true; + + UiLabel { + Layout.fillWidth: true; + textFormat: Text.RichText; + text: "ProROV settings"; + font.pointSize: Style.headerFontSize; + } + + UiButton { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter; + width: 32; + icon: icons.fa_times; + frameless: true; + onClicked: { + vehiclePanel.visible = false; + } + } + } + + Row { + spacing: 8; + + UiLabel { + Layout.fillWidth: true; + textFormat: Text.RichText; + text: "Servos"; + font.pointSize: Style.headerFontSize; + } + + UiButton { + id: servosResetButton; + label.text: "Reset"; + Layout.alignment: Qt.AlignVCenter; + + onClicked: { + control0.reset(); + control1.reset(); + control2.reset(); + } + } + + UiCheckbox { + id: hideUnset; + label.text: "Hide unset items" + Layout.alignment: Qt.AlignVCenter; + } + } + + RowLayout { + Layout.alignment: Qt.AlignTop; + Layout.fillWidth: true; + spacing: 32; + + GridLayout { + Layout.alignment: Qt.AlignTop; + id: servosGrid; + columns: 1; + property bool compact: hideUnset.checked; + rowSpacing: 0; + columnSpacing: 0; + + UiLabel { + text: "FCU 0"; + visible: !servosGrid.compact; + } + + RovServoControl { + id: control0; + device: 0; + } + + UiLabel { + text: "FCU 1"; + visible: !servosGrid.compact; + } + + RovServoControl { + id: control1; + device: 1; + } + + UiLabel { + text: "Onboard"; + visible: !servosGrid.compact; + } + + RovServoControl { + id: control2; + device: 2; + } + + Item { + width: parent.width; + height: 1; + Layout.fillHeight: true; + } + } + + ColumnLayout { + Layout.alignment: Qt.AlignCenter; + + GamepadWidget {} + + Item { + height: 64; + } + + Row { + UiLabel { + text: "Motors probing: " + probingPower.value; + } + } + + Slider { + id: probingPower; + from: -100; + to: 100; + value: 50; + stepSize: 10; + } + + Repeater { + model: 2; + + Row { + property real fcuIndex: modelData; + spacing: 4; + + UiLabel { + text: "FCU_" + modelData + ": "; + } + + Repeater { + model: 4; + + UiButton { + label.text: modelData; + + onClicked: { + Controllers.image.sendRovControl(fcuIndex + 1, modelData, probingPower.value); + } + + onPressing: { + Controllers.image.sendRovControl(fcuIndex + 1, modelData, probingPower.value); + } + } + } + } + } + + Item { + Layout.fillHeight: true; + } + + } + + } + + Item { + Layout.fillHeight: true; + } + + } +} diff --git a/resources/qml/Ui/RovServoControl.qml b/resources/qml/Ui/RovServoControl.qml new file mode 100644 index 0000000..6a202a7 --- /dev/null +++ b/resources/qml/Ui/RovServoControl.qml @@ -0,0 +1,97 @@ +import QtQml 2.2 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import mur.GamepadAxes 1.0 + +ColumnLayout { + id: root; + Layout.alignment: Qt.AlignTop; + + property int device: 0; + property int count: 4; + property bool compact: parent.compact; + + signal reset(); + Layout.margins: 0; + + Item { + id: separator; + height: 1; + width: parent.width; + } + + Repeater { + model: root.count; + + RowLayout { + id: row; + property bool active: selector.currentIndex != 0; + spacing: 4; + Layout.fillWidth: true; + Layout.margins: root.compact ? 2 : 0; + opacity: active ? 1.0 : 0.5; + visible: !root.compact || active; + + UiLabel { + font.family: Style.fontMono; + text: " " + (modelData + 1); + visible: !root.compact; + } + + Slider { + id: sliderServo; + property int index: modelData; + from: -100; + to: 100; + value: 0; + stepSize: 10; + + onValueChanged: { + Controllers.image.setServoValue(root.device, index, value); + } + Connections { + target: root; + onReset: function() { + sliderServo.value = 0; + } + } + Connections { + target: Controllers.image; + onServoValueChanged: function(device, servo, value) { + if (device === root.device && servo === sliderServo.index) { + sliderServo.value = value; + } + } + } + } + + UiLabel { + Layout.minimumWidth: 32; + font.family: Style.fontMono; + text: sliderServo.value; + horizontalAlignment: Text.AlignRight; + } + + ComboBox { + id: selector; + palette.buttonText: Style.lighterGray; + model: Controllers.gamepad.getAllFunctionNames(); + currentIndex: Controllers.gamepad.getProrovFunction(device, sliderServo.index); + + onActivated: function(index) { + Controllers.gamepad.setProrovFunction(device, sliderServo.index, currentIndex); + } + + Connections { + target: Controllers.gamepad; + onProrovFunctionsChanged: { + selector.currentIndex = Controllers.gamepad.getProrovFunction(device, sliderServo.index); + root.reset(); + } + } + } + } + } +} diff --git a/resources/qml/Ui/ScrollView.qml b/resources/qml/Ui/ScrollView.qml index e7fb82d..9dddc82 100644 --- a/resources/qml/Ui/ScrollView.qml +++ b/resources/qml/Ui/ScrollView.qml @@ -1,6 +1,5 @@ import QtQuick 2.2 import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 ScrollView { @@ -12,7 +11,7 @@ ScrollView { Rectangle { width: 7; height: control.viewport.height * control.viewport.height / control.contentItem.height - 4; - color: "#21252B"; + color: Style.bgDark; radius: 4; } } diff --git a/resources/qml/Ui/SettingsGamepad.qml b/resources/qml/Ui/SettingsGamepad.qml new file mode 100644 index 0000000..093f4a2 --- /dev/null +++ b/resources/qml/Ui/SettingsGamepad.qml @@ -0,0 +1,219 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.12 +import QtQml 2.2 +import QtQuick.Layouts 1.12 + +import mur.GamepadAxes 1.0 + +Item { + property string tabTitle: "Gamepad"; + property bool restartRequired: false; + anchors.fill: parent; + + function save() { + Controllers.joystick.saveSettings(); + } + + RowLayout { + id: column; + property int buttonWidth: 60; + spacing: 8; + anchors.margins: 8; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + + ColumnLayout { + Layout.alignment: Qt.AlignTop; + Layout.fillWidth: true; + + UiLabel { + text: "Axes and buttons:"; + font.pointSize: Style.headerFontSize; + } + + Repeater { + model: [ + GamepadAxes.AxisX, + GamepadAxes.AxisY, + GamepadAxes.AxisZ, + GamepadAxes.AxisW, + ]; + + RowLayout { + visible: modelData != GamepadAxes.AxisW; + + UiLabel { + Layout.fillWidth: true; + text: Controllers.joystick.getMovementAxisName(modelData) + ":"; + } + + UiButton { + width: column.buttonWidth; + label.font.bold: true; + outline: true; + label.opacity: label.text == "···" ? 0.25 : 1.0; + label.text: Controllers.joystick.allAxesBindings[modelData]; + highlight: Controllers.joystick.rebindingAxis === modelData || Math.abs(Controllers.joystick.allAxes[modelData]) > 50; + onClicked: { + Controllers.joystick.requestRebind(modelData); + } + } + + UiButton { + highlight: Controllers.joystick.allAxesInversions[modelData]; + icon: icons.fa_exchange; + toolTip: "Inverse"; + onClicked: { + Controllers.joystick.setAxisInversion(modelData, !highlight); + } + } + + UiButton { + icon: icons.fa_times; + toolTip: "Clear"; + onClicked: { + Controllers.joystick.clearAxis(modelData); + } + } + } + } + Repeater { + model: [ + ["Speed", GamepadAxes.SpeedSlow, GamepadAxes.SpeedFast], + ]; + + RowLayout { + visible: index == 0 || showAll.checked; + + UiLabel { + Layout.fillWidth: true; + text: modelData[0] + ":\t"; + } + + UiButton { + label.font.bold: true; + outline: true; + label.opacity: label.text == "···" ? 0.25 : 1.0; + width: column.buttonWidth; + label.text: Controllers.joystick.allAxesBindings[modelData[1]]; + highlight: Controllers.joystick.rebindingAxis === modelData[1] || Controllers.joystick.allAxes[modelData[1]] > 50; + onClicked: { + Controllers.joystick.requestRebind(modelData[1]); + } + } + + UiButton { + frameless: true; + enabled: false; + width: 25; + icon: Controllers.joystick.allAxes[modelData[2]] > 50 ? icons.fa_chevron_up : + Controllers.joystick.allAxes[modelData[1]] > 50 ? icons.fa_chevron_down : + "·"; + } + + + UiButton { + label.font.bold: true; + outline: true; + label.opacity: label.text == "···" ? 0.25 : 1.0; + width: column.buttonWidth; + label.text: Controllers.joystick.allAxesBindings[modelData[2]]; + highlight: Controllers.joystick.rebindingAxis === modelData[2] || Controllers.joystick.allAxes[modelData[2]] > 50; + onClicked: { + Controllers.joystick.requestRebind(modelData[2]); + } + } + + UiButton { + highlight: Controllers.joystick.allAxesInversions[modelData[1]]; + icon: icons.fa_exchange; + toolTip: "Inverse"; + onClicked: { + Controllers.joystick.swapAxes(modelData[1], modelData[2]); + } + } + + UiButton { + icon: icons.fa_times; + toolTip: "Clear"; + onClicked: { + Controllers.joystick.clearAxis(modelData[1]); + Controllers.joystick.clearAxis(modelData[2]); + } + } + } + } + } + + ColumnLayout { + Layout.alignment: Qt.AlignCenter; + Layout.fillWidth: true; + + GamepadWidget { + Layout.bottomMargin: 56; + Layout.alignment: Qt.AlignHCenter; + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter; + spacing: 4; + + UiLabel { + text: "Deadzone:" + } + + Slider { + Layout.alignment: Qt.AlignVCenter; + Layout.fillWidth: true; + from: 0; + to: 90; + value: Controllers.joystick.getDeadzone(); + stepSize: 1; + + onValueChanged: { + Controllers.joystick.setDeadzone(value); + } + } + + UiLabel { + font.family: Style.fontMono; + text: Controllers.joystick.deadzone; + horizontalAlignment: Text.AlignRight; + Layout.minimumWidth: 22; + } + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter; + spacing: 4; + + UiLabel { + text: "Exp factor:" + } + + Slider { + Layout.alignment: Qt.AlignVCenter; + Layout.fillWidth: true; + from: 1.0; + to: 5.0; + value: Controllers.joystick.getExpFactor(); + snapMode: Slider.SnapAlways + stepSize: 0.5; + + onValueChanged: { + value = value.toFixed(1); + Controllers.joystick.setExpFactor(value); + } + } + + UiLabel { + font.family: Style.fontMono; + text: Controllers.joystick.expFactor.toFixed(1); + horizontalAlignment: Text.AlignRight; + Layout.minimumWidth: 22; + } + } + } + } +} diff --git a/resources/qml/Ui/SettingsNetwork.qml b/resources/qml/Ui/SettingsNetwork.qml new file mode 100644 index 0000000..86dfa46 --- /dev/null +++ b/resources/qml/Ui/SettingsNetwork.qml @@ -0,0 +1,85 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.12 +import QtQml 2.2 +import QtQuick.Layouts 1.12 + +GridLayout { + property string tabTitle: "Network"; + property bool restartRequired: true; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.margins: 8; + Layout.fillWidth: true; + Layout.fillHeight: false; + columns: 2; + + function save() { + Controllers.network.setConnectionAddress(addressEdit.text); + Controllers.network.setReconnectTime(reconnectTimeEdit.text * 1); + Controllers.network.setPingTime(pingTimeEdit.text * 1); + Controllers.network.setPongTime(pongTimeEdit.text * 1); + + Controllers.network.saveSettings(); + } + + UiLabel { + text: "Address and port:" + } + + UiTextInput { + id: addressEdit; + text: Controllers.network.getConnectionAddress(); + } + + UiLabel { + text: "Timers (msec):" + } + + UiLabel {} + + UiLabel { + text: "Reconnection:" + } + + UiTextInput { + id: reconnectTimeEdit; + + validator: IntValidator { + bottom: 500; + top: 10000; + } + + text: Controllers.network.getReconnectTime(); + } + + UiLabel { + text: "Ping (check):" + } + + UiTextInput { + id: pingTimeEdit; + + validator: IntValidator { + bottom: 500; + top: 10000; + } + + text: Controllers.network.getPingTime(); + } + + UiLabel { + text: "Pong (timeout):" + } + + UiTextInput { + id: pongTimeEdit; + + validator: IntValidator { + bottom: 500; + top: 10000; + } + + text: Controllers.network.getPongTime(); + } +} diff --git a/resources/qml/Ui/SettingsPanel.qml b/resources/qml/Ui/SettingsPanel.qml new file mode 100644 index 0000000..e5e6241 --- /dev/null +++ b/resources/qml/Ui/SettingsPanel.qml @@ -0,0 +1,112 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.12 +import QtQml 2.2 +import QtQuick.Layouts 1.12 + +Popup { + id: settingsPopup; + x: (parent.width - width) / 2 + y: menuBar.y + width: parent.width / 1; + height: parent.height + menuBar.height; + closePolicy: Popup.CloseOnEscape; + modal: true; + visible: true; + parent: fullWindow; + + background: Rectangle { + id: back; + anchors.fill: parent; + color: Style.bgDark; + border.width: 1; + border.color: Style.bgDarker; + } + + Overlay.modal: Rectangle { + color: Style.overlayBack; + Behavior on opacity { + NumberAnimation { + duration: Style.animFastest + } + } + } + + ColumnLayout { + anchors.top: parent.top; + anchors.bottom: parent.bottom; + width: Math.min(parent.width, 900); + anchors.horizontalCenter: parent.horizontalCenter; + + Row { + Layout.alignment: Qt.AlignHCenter; + + UiLabel { + anchors.verticalCenter: parent.verticalCenter; + font.pointSize: Style.headerFontSize; + text: settingsTabs.visibleItem.tabTitle + " settings"; + } + } + + Rectangle { + Layout.fillWidth: true; + width: parent.width; + height: 1; + color: Style.bgBlue; + } + + TabView { + id: settingsTabs; + Layout.fillWidth: true; + Layout.fillHeight: true; + + SettingsRemote { + id: settingsRemote; + } + + SettingsNetwork { + id: settingsNetwork; + } + + SettingsGamepad { + id: settingsGamepad; + } + } + } + + Row { + anchors.right: parent.right; + anchors.bottom: parent.bottom; + + UiButton { + id: settingsOkButton; + anchors.verticalCenter: parent.verticalCenter; + label.text: "OK"; + label.font.bold: true; + onClicked: { + settingsGamepad.save(); + settingsPopup.close(); + } + visible: !settingsTabs.visibleItem.restartRequired; + } + + UiButton { + id: settingsRestartButton; + anchors.verticalCenter: parent.verticalCenter; + label.text: "Save"; + toolTip: "You may need to restart"; + label.font.bold: true; + visible: settingsTabs.visibleItem.restartRequired; + onClicked: { + settingsOkButton.click(); + settingsRemote.save(); + settingsNetwork.save(); + } + } + } + + onVisibleChanged: { + if (visible) { + settingsRemote.load(); + } + } +} diff --git a/resources/qml/Ui/SettingsRemote.qml b/resources/qml/Ui/SettingsRemote.qml new file mode 100644 index 0000000..c914fc8 --- /dev/null +++ b/resources/qml/Ui/SettingsRemote.qml @@ -0,0 +1,148 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.12 +import QtQml 2.2 +import QtQuick.Layouts 1.12 + +Item { + id: settingsRemoteTab; + property string tabTitle: "Remote mode"; + property bool dangerMode: false; + property bool restartRequired: true; + anchors.fill: parent; + anchors.margins: 8; + + property var controller: Controllers.image; + + function save() { + Controllers.image.setPipelines( + watermarkEdit.text + ); + + Controllers.image.saveSpeedLimits(); + } + + function load() { + var pipes = Controllers.image.getPipelines(); + watermarkEdit.text = pipes[3]; + } + + GridLayout { + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + Layout.fillWidth: true; + Layout.fillHeight: false; + columnSpacing: 8; + columns: 2 + + UiLabel { /* placeholder */ } + + UiLabel { + text: "Movements:"; + font.pointSize: Style.headerFontSize; + } + + UiLabel { + Layout.alignment: Qt.AlignTop; + text: "Speed limits:" + } + + ColumnLayout { + Layout.alignment: Qt.AlignTop; + + Repeater { + id: powerGroup; + + model: 3; + + RowLayout { + UiLabel { + horizontalAlignment: Text.AlignLeft; + Layout.minimumWidth: 70; + text: ["Low: ", "Normal: ", "Max: "][index]; + } + + Slider { + id: powerSlider; + Layout.maximumWidth: 200; + Layout.fillWidth: true; + from: 0; + to: 100; + stepSize: 5; + snapMode: Slider.SnapAlways; + value: Controllers.image.getSpeedLimits()[index]; + + onValueChanged: { + let min = index == 0 ? 0 : + index == 1 ? Controllers.image.speedLimits[0] : + Controllers.image.speedLimits[1] ; + + let max = index == 0 ? Controllers.image.speedLimits[1] : + index == 1 ? Controllers.image.speedLimits[2] : + 100; + + value = Math.max(Math.min(value, max), min); + Controllers.image.setSpeedLimit(index, value); + } + } + + UiLabel { + Layout.minimumWidth: 30; + font.family: Style.fontMono; + Layout.alignment: Qt.AlignHCenter; + text: powerSlider.value.toFixed(0); + horizontalAlignment: Text.AlignRight; + } + } + } + } + + + UiLabel { /* placeholder */ } + + UiLabel { + text: "Video streaming:"; + font.pointSize: Style.headerFontSize; + } + + UiLabel { + text: "Watermark:" + Layout.alignment: Qt.AlignTop; + } + + ColumnLayout { + RowLayout { + spacing: 8; + + UiButton { + label.text: "Default"; + onClicked: { + watermarkEdit.text = "%device% / %date% / Depth: %depth% M / Temp: %temp% C / Yaw: %yaw%"; + } + } + + UiCheckbox { + label.text: "Enable watermark"; + from: Controllers.image; + bind: "watermarkOn"; + } + } + + UiTextEdit { + id: watermarkEdit; + text: "%device% / %date% / Depth: %depth% M / Temp: %temp% C / Yaw: %yaw%"; + } + + UiLabel { + text: "Available variables: date, device, depth, temp, yaw, roll, pitch."; + wrapMode: "WordWrap"; + font: Style.fontMono; + Layout.fillWidth: true; + } + } + + UiLabel { + text: "" + } + } +} diff --git a/resources/qml/Ui/SplitView.qml b/resources/qml/Ui/SplitView.qml index be68825..b6714a6 100644 --- a/resources/qml/Ui/SplitView.qml +++ b/resources/qml/Ui/SplitView.qml @@ -10,7 +10,7 @@ Item { property int minRight: 50; property int minLeft: 50; - property color handleBorder: ma.containsMouse || ma.pressed ? "#3C424F" : leftContainer.minimized || rightContainer.minimized ? "#282C34" : "#21252B"; + property color handleBorder: ma.containsMouse || ma.pressed ? Style.darkerGray : leftContainer.minimized || rightContainer.minimized ? Style.bgBlue : Style.bgDarker; function setSplitLocation(val) { anchor.x = val; @@ -134,8 +134,9 @@ Item { height: leftIcon.height * 2; radius: 3; - color: "#181A1F"; + color: Style.bgDarker; border.color: root.handleBorder; + opacity: ma.containsMouse ? 1.0 : 0.75; Icon { id: leftIcon; @@ -170,8 +171,9 @@ Item { height: rightIcon.height * 2; radius: 3; - color: "#181A1F"; + color: Style.bgDarker; border.color: root.handleBorder; + opacity: ma.containsMouse ? 1.0 : 0.75; Icon { id: rightIcon; @@ -214,4 +216,11 @@ Item { onPressed: root.unlockAnchor(); onReleased: root.lockAnchor(); } + + Behavior on handleBorder { + ColorAnimation { + duration: Style.animFastest; + easing.type: Style.animEasing; + } + } } diff --git a/resources/qml/Ui/Style.qml b/resources/qml/Ui/Style.qml new file mode 100644 index 0000000..842daa9 --- /dev/null +++ b/resources/qml/Ui/Style.qml @@ -0,0 +1,58 @@ +pragma Singleton +import QtQuick 2.11 + +QtObject { + property color transparent: "#00000000"; + property color shadowColor: "#C0000000"; + property color outlineColor:"#80000000"; + + property color lighterGray: "#D6DBDF"; + property color lightGray: "#9DA5B4"; + property color gray: "#6E7582"; + property color darkGray: "#5E636E"; + property color darkerGray: "#4C515A"; + + property color green: "#148F77"; + property color greenDark: "#044F47"; + property color yellow: "#FFEF38"; + property color yellowDark: "#DAB709"; + property color orange: "#F39C12"; + property color orangeDark: "#A35C02"; + property color red: "#E74C3C"; + + property color lightestBlue:"#6DAFF2"; + property color lightBlue: "#1D9FF2"; + property color blue: "#2B68A4"; + + property color bgDarker: "#181A1F"; + property color bgDark: "#21252B"; + property color bgBlue: "#282C34"; + property color semiDarkest: "#333943"; + property color semiDarker: "#363C46"; + property color semiDark: "#3F4552"; + property color white: "#FFFFFF"; + + property color overlayBack: "#80202020"; + + property color batteryColor: { + if (!Controllers.network.connected) { + batteryColor: darkGray; + } else if (Controllers.network.battery >= 50) { + batteryColor: green; + } else if (Controllers.network.battery >= 20) { + batteryColor: yellowDark; + } else { + batteryColor: red; + } + } + + property string fontMono: "Noto Mono"; + property string fontSans: "Noto Sans"; + + property real headerFontSize: 16; + + property double animDuration: 500; + property double animFast: 250; + property double animFastest: 150; + property int animEasing: Easing.OutQuart; +} diff --git a/resources/qml/Ui/TabView.qml b/resources/qml/Ui/TabView.qml index 40733cc..9f46c33 100644 --- a/resources/qml/Ui/TabView.qml +++ b/resources/qml/Ui/TabView.qml @@ -1,4 +1,4 @@ -import QtQuick 2.9 +import QtQuick 2.12 Item { id: root; @@ -7,6 +7,11 @@ Item { property var visibleItem: undefined; property int selectedIndex: -1; + property int itemsCount: container.children.length; + + function selectTab(index) { + selectedIndex = index; + } Item { id: container; @@ -33,7 +38,7 @@ Item { anchors.right: parent.right; anchors.bottom: parent.bottom; - color: "#21252B"; + color: Style.bgDark; height: 22; clip: true; @@ -54,12 +59,10 @@ Item { anchors.bottom: parent.bottom; anchors.bottomMargin: 1; radius: 4; - - color: index == root.selectedIndex ? "#282C34" : "#18191E"; + color: index == root.selectedIndex ? Style.bgDarker : Style.bgDark; UiLabel { id: label; - //font.pixelSize: 12; anchors.centerIn: parent; anchors.verticalCenterOffset: panel.radius/2; @@ -87,7 +90,7 @@ Item { anchors.top: parent.top; height: 1; - color: "#181A1F" + color: Style.bgDarker } } diff --git a/resources/qml/Ui/TagLabel.qml b/resources/qml/Ui/TagLabel.qml index 236f330..8abd4c9 100644 --- a/resources/qml/Ui/TagLabel.qml +++ b/resources/qml/Ui/TagLabel.qml @@ -9,7 +9,7 @@ Rectangle { property alias label: label; color: "#008800"; - border.color: "#181A1F" + border.color: Style.bgDarker radius: 2; diff --git a/resources/qml/Ui/TelemetryGroup.qml b/resources/qml/Ui/TelemetryGroup.qml new file mode 100644 index 0000000..641f9cf --- /dev/null +++ b/resources/qml/Ui/TelemetryGroup.qml @@ -0,0 +1,24 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQml 2.2 +import QtQuick.Layouts 1.12 + +Repeater { + id: group; + property var color: fullWindow.visible ? Style.white : Style.orange; + + Label { + id: label; + property bool onRight: index % 2 == 1; + Layout.fillWidth: index % 2 == 1; + horizontalAlignment: onRight ? Text.AlignRight : Text.AlignLeft; + visible: group.visible; + text: modelData; + + topPadding: 2; + bottomPadding: 2; + color: group.color; + font.family: Style.fontMono; + } +} + diff --git a/resources/qml/Ui/TelemetryPopup.qml b/resources/qml/Ui/TelemetryPopup.qml new file mode 100644 index 0000000..2a1ef04 --- /dev/null +++ b/resources/qml/Ui/TelemetryPopup.qml @@ -0,0 +1,174 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQml 2.2 +import QtQuick.Layouts 1.12 + +Popup { + id: popup; + x: (parent.x + parent.width - popup.width) + 1; + y: (parent.y + parent.height * 2) / 2 - 1; + closePolicy: Popup.NoAutoClose; + visible: !Controllers.scripts.local; + padding: 10; + width: Math.max(contentWidth + padding * 2, 135); + font.family: Style.fontMono; + font.pointSize: 10; + opacity: fullWindow.visible ? 0.8 : 1.0; + + background: Rectangle { + anchors.fill: parent; + color: Style.bgDark; + border.width: 1; + border.color: Style.bgDarker; + opacity: fullWindow.visible ? 0.75 : 1.0; + } + + ColumnLayout { + anchors.fill: parent; + + Label { + text: "Telemetry"; + color: fullWindow.visible ? Style.white : Style.lightGray; + font.family: Style.fontMono; + } + + GridLayout { + Layout.fillWidth: true; + columns: 2; + rowSpacing: 2; + columnSpacing: 4; + + TelemetryGroup { + model: [ + "Yaw:", Controllers.network.yaw.toFixed(2), + "Pitch:", Controllers.network.pitch.toFixed(2), + "Roll:", Controllers.network.roll.toFixed(2), + ]; + } + + TelemetryGroup { + visible: !Controllers.network.usv; + model: [ + "Depth:", Controllers.network.depth.toFixed(2), + "Temp:", Controllers.network.temperature.toFixed(2), + ]; + } + + TelemetryGroup { + visible: Controllers.network.usv; + model: [ + "Latitude:", Controllers.network.latitude.toFixed(2), + "Longitude:", Controllers.network.longitude.toFixed(2), + "Satellites:", Controllers.network.satellites.toFixed(0), + "Altitude:", Controllers.network.altitude.toFixed(2), + "Speed:", Controllers.network.speed.toFixed(2), + ]; + } + + TelemetryGroup { + visible: Controllers.network.rov; + model: [ + "FCU 0", "", + "- Volt:", Controllers.network.fcu_telemetry[0].toFixed(2), + "- Amp:", Controllers.network.fcu_telemetry[1].toFixed(2), + + "FCU 1", "", + "- Volt:", Controllers.network.fcu_telemetry[2].toFixed(2), + "- Amp:", Controllers.network.fcu_telemetry[3].toFixed(2), + ]; + } + + TelemetryGroup { + visible: !Controllers.network.rov; + model: [ + "Battery:", Controllers.network.battery.toFixed(0) + "%", + ]; + color: Style.batteryColor; + } + } + + Rectangle { + width: 60; + height: width; + Layout.alignment: Qt.AlignHCenter; + color: "transparent"; + border.width: 2; + border.color: "#44FFFFFF"; + clip: true; + + Rectangle { + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + height: parent.height - 4; + width: 2; + color: "#44FFFFFF"; + } + + Rectangle { + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + width: parent.width - 4; + height: 2; + color: "#44FFFFFF"; + } + + Rectangle { + id: pitcher; + y: parent.height / 2 - height / 2 - Controllers.network.pitch / 2; + anchors.horizontalCenter: parent.horizontalCenter; + width: parent.width / 1.5; + height: width / 8; + color: "white"; + opacity: 0.5; + antialiasing: true; + radius: 3; + rotation: Controllers.network.roll; + + Behavior on rotation { + NumberAnimation { + duration: Style.animFastest; + easing.type: Easing.InOutQuart; + } + } + + Behavior on y { + NumberAnimation { + duration: Style.animFastest; + easing.type: Easing.InOutQuart; + } + } + } + + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter; + y: parent.height / 2 - height / 2 - Controllers.network.pitch / 2; + width: 5; + height: 5; + radius: 5; + color: "white"; + opacity: 0.75; + + Behavior on y { + NumberAnimation { + duration: Style.animFastest; + easing.type: Easing.InOutQuart; + } + } + } + } + + } + + enter: Transition { + NumberAnimation { + property: "x"; + from: x + width; + to: x; + easing.type: Style.animEasing; + duration: Style.animDuration; + } + } +} + + + diff --git a/resources/qml/Ui/TelimetryPopup.qml b/resources/qml/Ui/TelimetryPopup.qml deleted file mode 100644 index b9208ee..0000000 --- a/resources/qml/Ui/TelimetryPopup.qml +++ /dev/null @@ -1,230 +0,0 @@ -import QtQuick 2.11 -import QtQuick.Controls 2.12 -import QtQml 2.2 -import QtQuick.Controls.Styles 1.4 - -Popup { - id: popup; -// x: (parent.x + parent.width - popup.width) / 2; -// y: (parent.y + parent.height * 2) / 2 - 1; - x: (parent.x + parent.width - popup.width); - y: (parent.y + parent.height * 2) / 2 - 1; - closePolicy: Popup.NoAutoClose; - visible: true; - - background: Rectangle { - anchors.fill: parent; - color: "#21252B"; - border.width: 1; - border.color: "#181A1F"; - } - Row { - Column { - Label { - bottomPadding: 3; - text: "Telemetry"; - font.pointSize: 9; - color: "#9DA5B4"; - } - Label { - topPadding: 2; - bottomPadding: 2; - text: "Yaw: ";/// + Controllers.network.yaw.toFixed(2); - color: "#F39C12"; - } - Label { - topPadding: 2; - bottomPadding: 2; - text: "Pitch: ";// + Controllers.network.pitch.toFixed(2); - color: "#F39C12"; - } - Label { - topPadding: 2; - bottomPadding: 2; - text: "Roll: ";// + Controllers.network.roll.toFixed(2); - color: "#F39C12"; - } - Label { - visible: !Controllers.network.usv; - topPadding: 2; - bottomPadding: 2; - text: "Depth: ";// + Controllers.network.depth.toFixed(2); - color: "#F39C12"; - } - - /* - * USV - */ - - Label { - visible: Controllers.network.usv; - topPadding: 2; - bottomPadding: 2; - text: "Latitude: ";// + Controllers.network.depth.toFixed(2); - color: "#F39C12"; - } - Label { - visible: Controllers.network.usv; - topPadding: 2; - bottomPadding: 2; - text: "Longitude: ";// + Controllers.network.depth.toFixed(2); - color: "#F39C12"; - } - Label { - visible: Controllers.network.usv; - topPadding: 2; - bottomPadding: 2; - text: "Satellites: ";// + Controllers.network.depth.toFixed(2); - color: "#F39C12"; - } - Label { - visible: Controllers.network.usv; - topPadding: 2; - bottomPadding: 2; - text: "Altitude: ";// + Controllers.network.depth.toFixed(2); - color: "#F39C12"; - } - Label { - visible: Controllers.network.usv; - topPadding: 2; - bottomPadding: 2; - text: "Speed: ";// + Controllers.network.depth.toFixed(2); - color: "#F39C12"; - } - Label { - topPadding: 2; - bottomPadding: 2; - text: "Battery: ";// + Controllers.network.pressure.toFixed(2); - color: Controllers.network.connected ? Controllers.network.battery < 20 ? "#E74C3C" : "#148F77" : "#626567"; - } - } - - Column { - Label { - bottomPadding: 3; - text: " "; - font.pointSize: 9; - color: "#9DA5B4"; - } - Label { - topPadding: 2; - bottomPadding: 2; - text: Controllers.network.yaw.toFixed(2); - color: "#F39C12"; - } - Label { - topPadding: 2; - bottomPadding: 2; - text: Controllers.network.pitch.toFixed(2); - color: "#F39C12"; - } - Label { - topPadding: 2; - bottomPadding: 2; - text: Controllers.network.roll.toFixed(2); - color: "#F39C12"; - } - Label { - visible: !Controllers.network.usv; - topPadding: 2; - bottomPadding: 2; - text: Controllers.network.depth.toFixed(2); - color: "#F39C12"; - } - - /* - * USV - */ - - Label { - visible: Controllers.network.usv; - topPadding: 2; - bottomPadding: 2; - text: Controllers.network.latitude.toFixed(2); - color: "#F39C12"; - } - - Label { - visible: Controllers.network.usv; - topPadding: 2; - bottomPadding: 2; - text: Controllers.network.longitude.toFixed(2); - color: "#F39C12"; - } - - Label { - visible: Controllers.network.usv; - topPadding: 2; - bottomPadding: 2; - text: Controllers.network.satellites.toFixed(0); - color: "#F39C12"; - } - - Label { - visible: Controllers.network.usv; - topPadding: 2; - bottomPadding: 2; - text: Controllers.network.altitude.toFixed(2); - color: "#F39C12"; - } - - Label { - visible: Controllers.network.usv; - topPadding: 2; - bottomPadding: 2; - text: Controllers.network.speed.toFixed(2); - color: "#F39C12"; - } - - Label { - topPadding: 2; - bottomPadding: 2; - text: Controllers.network.battery.toFixed(0) + "%"; - color: Controllers.network.connected ? Controllers.network.battery < 20 ? "#E74C3C" : "#148F77" : "#626567"; - } - } - } -/* - Column { - Label { - bottomPadding: 3; - text: "Telemetry"; - font.pointSize: 9; - color: "#9DA5B4"; - } - Label { - topPadding: 2; - bottomPadding: 2; - text: "Yaw: " + Controllers.network.yaw.toFixed(2); - color: "#F39C12"; - } - Label { - topPadding: 2; - bottomPadding: 2; - text: "Pitch: " + Controllers.network.pitch.toFixed(2); - color: "#F39C12"; - } - Label { - topPadding: 2; - bottomPadding: 2; - text: "Roll: " + Controllers.network.roll.toFixed(2); - color: "#F39C12"; - } - Label { - topPadding: 2; - bottomPadding: 2; - text: "Depth: " + Controllers.network.depth.toFixed(2); - color: "#F39C12"; - } - Label { - topPadding: 2; - bottomPadding: 2; - text: "Pressure: " + Controllers.network.pressure.toFixed(2); - color: "#F39C12"; - } - } - */ -} - - - diff --git a/resources/qml/Ui/UiButton.qml b/resources/qml/Ui/UiButton.qml index 7655a47..160526e 100644 --- a/resources/qml/Ui/UiButton.qml +++ b/resources/qml/Ui/UiButton.qml @@ -1,6 +1,7 @@ import QtQuick 2.9 import QtQuick.Controls 2.2 + Rectangle { id: root; width: frameless ? row.width + 6 : row.width + 20; @@ -9,35 +10,71 @@ Rectangle { property alias label: label; property alias labelColor: label.color; property alias iconColor: icon.color; + property alias iconOpacity: icon.opacity; property alias iconSize: icon.font.pointSize; + property alias iconRotation: icon.rotation; property alias icon: icon.icon; property alias icons: icon.icons; + property alias after_item: after_item; + property alias shortcut: shortcut; + property alias ma: ma; property bool highlight: false; property bool frameless: false; property string toolTip: ""; + property bool outline: fullWindow.visible ? true : false; + + opacity: visible ? 1.0 : 0.0; - color: frameless ? "#00000000" : highlight ? "#2B68A4" : ma.pressed ? "#181A1F" : ma.containsMouse ? "#363C46" : "#333842" - border.color: frameless ? "#00000000" : "#181A1F" + color: frameless ? Style.transparent : highlight ? Style.blue : ma.pressed ? Style.semiDarkest : ma.containsMouse ? Style.semiDark : Style.semiDarker; + border.color: frameless ? Style.transparent : Style.bgDarker; radius: 2; signal clicked; + signal pressing; Row { id: row; anchors.centerIn: parent; - spacing: 4; + spacing: 6; Icon { id: icon; anchors.verticalCenter: parent.verticalCenter; color: label.color; + width: text.length > 0 ? font.pixelSize : 0; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + style: Text.Outline; + styleColor: Style.outlineColor; + } + + Item { + id: after_item; + anchors.top: parent.top; + anchors.bottom: parent.bottom; } UiLabel { id: label; anchors.verticalCenter: parent.verticalCenter; font.weight: Font.Medium; - color: enabled ? ma.pressed ? "#1D9FF2" : highlight || ma.containsMouse ? "#fff" : "#9DA5B4" : "#6E7582"; + color: enabled ? ma.pressed ? Style.lightBlue : highlight || ma.containsMouse ? Style.white : Style.lightGray : Style.darkGray; + + style: outline ? Text.Outline : Text.Normal; + styleColor: Style.outlineColor; + font.bold: outline; + } + } + + + Timer { + id: repeatedPressing; + repeat: true; + running: false; + interval: 100; + + onTriggered: { + root.pressing(); } } @@ -51,6 +88,10 @@ Rectangle { root.forceActiveFocus(); root.clicked(); } + + onPressedChanged: { + repeatedPressing.running = pressed; + } } ToolTip { @@ -60,5 +101,39 @@ Rectangle { timeout: 5000 } + Shortcut { + id: shortcut; + autoRepeat: false; + context: Qt.ApplicationShortcut; + onActivated: { + if (root.enabled) { + root.clicked() + } + } + } + + function click() { + root.clicked(); + } + + Behavior on opacity { + NumberAnimation { + duration: 500; + easing.type: Style.animEasing; + } + } + + Behavior on width { + NumberAnimation { + duration: 250; + easing.type: Style.animEasing; + } + } + Behavior on color { + ColorAnimation { + duration: Style.animFastest; + easing.type: Style.animEasing; + } + } } diff --git a/resources/qml/Ui/UiCheckbox.qml b/resources/qml/Ui/UiCheckbox.qml index f4ddff0..25ca461 100644 --- a/resources/qml/Ui/UiCheckbox.qml +++ b/resources/qml/Ui/UiCheckbox.qml @@ -3,26 +3,28 @@ import QtQuick 2.9 Item { id: root; - width: row.width + 24; - height: label.height + 6; + width: row.width; + height: label.height + 4; + property alias label: label; property bool checked: false; - property var from; - property string bind; + property var from: root; + property string bind: "checked"; signal clicked; Row { id: row; - anchors.centerIn: parent; + anchors.left: parent.left; + anchors.top: parent.top; spacing: 4; Rectangle { anchors.verticalCenter: parent.verticalCenter; width: 16; height: 16; - color: ma.pressed ? "#181A1F" : ma.containsMouse ? "#363C46" : "#333842" - border.color: "#181A1F"; + color: ma.pressed ? Style.bgDarker : ma.containsMouse ? "#363C46" : "#333842" + border.color: Style.bgDarker; radius: 2; Icon { @@ -38,7 +40,7 @@ Item { id: label; anchors.verticalCenter: parent.verticalCenter; font.weight: Font.Medium; - color: enabled ? ma.containsMouse ? "#fff" : "#9DA5B4" : "#6E7582"; + color: enabled ? ma.containsMouse ? "#fff" : "#9DA5B4" : Style.gray; } } diff --git a/resources/qml/Ui/UiLabel.qml b/resources/qml/Ui/UiLabel.qml index 6e040e4..bf6af92 100644 --- a/resources/qml/Ui/UiLabel.qml +++ b/resources/qml/Ui/UiLabel.qml @@ -1,11 +1,11 @@ import QtQuick 2.2 + Text { - color: enabled ? "#9DA5B4" : "#6E7582" + color: enabled ? Style.lightGray : Style.gray; - font.family: "Segoe WPC"; + font.family: Style.fontSans; font.pointSize: 10; - //renderType: TextEdit.NativeRendering; elide: Text.ElideRight; textFormat: Text.PlainText; diff --git a/resources/qml/Ui/UiTextArea.qml b/resources/qml/Ui/UiTextArea.qml new file mode 100644 index 0000000..109e020 --- /dev/null +++ b/resources/qml/Ui/UiTextArea.qml @@ -0,0 +1,98 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.12 + +TextArea { + id: root; + + property var refocus; + property var from; + + property string bind; + property string lastInput; + property string newInput; + + property bool immediate: false; + property bool resetInput: false; + property bool clearFocusOnEnter: true; + + property alias radius: rect.radius; + + property color borderDefault: Style.bgDarker + property color borderActive: "#9DA5B4" + + leftPadding: 4; + rightPadding: 4; + + color: enabled ? "#fff" : "#9DA5B4"; + selectedTextColor: "#000"; + selectionColor: root.borderActive; + + font.family: Style.fontMono; + font.pointSize: 13; + verticalAlignment: TextInput.AlignVCenter; + + activeFocusOnPress: true; + selectByMouse: true; + clip: true; + + Rectangle { + id: rect; + + anchors.left: parent.left; + anchors.right: parent.right; + anchors.verticalCenter: parent.verticalCenter; + + z: parent.z - 1; + height: root.contentHeight; + + color: "#333842" + border.color: root.activeFocus ? root.borderActive : root.borderDefault; + } + + onActiveFocusChanged: { + if (activeFocus) { + root.lastInput = text; + } else { + if (root.resetInput) { + applyText(newInput); + } else { + applyText(text); + } + } + } + + onTextChanged: { + if (root.immediate) { + applyText(text); + } + } + + function applyText(newText) { + if (root.from) { + text = newText; + root.from[root.bind] = newText; + } + } + + function clearFocus() { + if (refocus) { + refocus.forceActiveFocus(); + } else { + root.parent.forceActiveFocus(); + } + } + + Keys.onPressed: { + var key = event.key; + + if (key === Qt.Key_Escape) { + Controllers.editor.search.visible = false; + event.accepted = true; + refocus.forceActiveFocus(); + } + else if ((key === Qt.Key_Enter || key === Qt.Key_Return) && clearFocusOnEnter === true) { + enterPressed(); + event.accepted = true; + } + } +} diff --git a/resources/qml/Ui/UiTextEdit.qml b/resources/qml/Ui/UiTextEdit.qml new file mode 100644 index 0000000..407f7e4 --- /dev/null +++ b/resources/qml/Ui/UiTextEdit.qml @@ -0,0 +1,34 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.12 + +Item { + id: root; + Layout.fillWidth: true; + Layout.alignment: Qt.AlignTop; + Layout.preferredHeight: edit.height; + property alias wrapMode: edit.wrapMode; + property alias text: edit.text; + Layout.bottomMargin: 4; + + Rectangle { + width: edit.width; + height: edit.height + edit.padding; + color: Style.bgDarker; + border.color: Style.semiDarkest; + border.width: 1; + } + + ApplicationTextEdit { + id: edit; + Layout.fillWidth: true; + height: contentHeight; + width: parent.width; + font.pointSize: 10; + font.family: Style.fontMono; + color: enabled ? Style.lighterGray : Style.darkGray; + wrapMode: "WordWrap"; + padding: 4; + topPadding: 2; + bottomPadding: 0; + } +} diff --git a/resources/qml/Ui/UiTextInput.qml b/resources/qml/Ui/UiTextInput.qml new file mode 100644 index 0000000..d77ee69 --- /dev/null +++ b/resources/qml/Ui/UiTextInput.qml @@ -0,0 +1,108 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.12 + +TextInput { + id: root; + + property var refocus; + property var from; + + property string bind; + property string lastInput; + property string newInput; + + property bool immediate: false; + property bool resetInput: false; + property bool clearFocusOnEnter: true; + + property alias radius: rect.radius; + property alias background: rect.color; + + property color borderDefault: Style.semiDarkest; + property color borderActive: Style.darkerGray; + + leftPadding: 4; + rightPadding: 4; + + color: enabled ? Style.white : Style.lightGray; + selectedTextColor: Style.white; + selectionColor: root.borderActive; + + font.family: Style.fontMono; + font.pointSize: 10; + verticalAlignment: TextInput.AlignVCenter; + + activeFocusOnPress: true; + selectByMouse: true; + clip: true; + + height: 18; + Layout.fillWidth: true; + + Rectangle { + id: rect; + + anchors.left: parent.left; + anchors.right: parent.right; + anchors.verticalCenter: parent.verticalCenter; + + z: parent.z - 1; + height: root.contentHeight; + + color: Style.bgDarker; + border.color: root.activeFocus ? root.borderActive : root.borderDefault; + } + + MouseArea { + anchors.fill: parent; + cursorShape: Qt.IBeamCursor; + acceptedButtons: Qt.NoButton; + } + + onActiveFocusChanged: { + if (activeFocus) { + root.lastInput = text; + } else { + if (root.resetInput) { + applyText(newInput); + } else { + applyText(text); + } + } + } + + onTextChanged: { + if (root.immediate) { + applyText(text); + } + } + + function applyText(newText) { + if (root.from) { + text = newText; + root.from[root.bind] = newText; + } + } + + function clearFocus() { + if (refocus) { + refocus.forceActiveFocus(); + } else { + root.parent.forceActiveFocus(); + } + } + + Keys.onPressed: { + var key = event.key; + + if (key === Qt.Key_Escape) { + Controllers.editor.search.visible = false; + event.accepted = true; + refocus.forceActiveFocus(); + } + else if ((key === Qt.Key_Enter || key === Qt.Key_Return) && clearFocusOnEnter === true) { + enterPressed(); + event.accepted = true; + } + } +} diff --git a/resources/qml/Ui/UpdatePopup.qml b/resources/qml/Ui/UpdatePopup.qml index d11ab8d..df48c73 100644 --- a/resources/qml/Ui/UpdatePopup.qml +++ b/resources/qml/Ui/UpdatePopup.qml @@ -1,7 +1,6 @@ import QtQuick 2.11 import QtQuick.Controls 2.12 import QtQml 2.2 -import QtQuick.Controls.Styles 1.4 Popup { id: updatePopup; @@ -10,26 +9,25 @@ Popup { closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside; modal: true; visible: true; - parent: ApplicationWindow.overlay; + parent: fullWindow; width: 350; - height: 120; + height: 200; property var controller: Controllers.updates; background: Rectangle { anchors.fill: parent; - color: "#21252B"; - border.width: 0; - border.color: "#181A1F"; + color: Style.bgDark; + border.width: 1; + border.color: Style.bgDarker; } Rectangle { - id: remoteHeader; height: 22; anchors.top: parent.top; anchors.left: parent.left; anchors.right: parent.right; - color: "#21252B"; + color: Style.bgDark; UiLabel { text: "Update settings"; @@ -42,7 +40,7 @@ Popup { anchors.bottom: remoteHeader.bottom; height: 1; - color: "#181A1F"; + color: Style.bgDarker; } Row { @@ -60,19 +58,23 @@ Popup { } } - Rectangle { - id: updateFooter; - height: 22; + Column { anchors.left: parent.left; anchors.right: parent.right; anchors.bottom: parent.bottom; - color: "#21252B"; Row { - anchors.right: parent.right; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.leftMargin: 8; + anchors.horizontalCenter: parent.horizontalCenter; + bottomPadding: 8; + UiCheckbox { + label.text: "Check for updates on start"; + from: controller; + bind: "checkUpdate"; + } + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter; spacing: 4; UiButton { @@ -85,7 +87,6 @@ Popup { controller.onUpdate(); updatePopup.close(); } - } } @@ -96,19 +97,6 @@ Popup { updatePopup.close(); } } - - } - - Row { - anchors.left: parent.left; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - - UiCheckbox { - label.text: "Check for updates"; - from: controller; - bind: "checkUpdate"; - } } } } diff --git a/resources/qml/Ui/qmldir b/resources/qml/Ui/qmldir new file mode 100644 index 0000000..693bace --- /dev/null +++ b/resources/qml/Ui/qmldir @@ -0,0 +1 @@ +singleton Style Style.qml diff --git a/resources/resources.qrc b/resources/resources.qrc index d6e4079..8310235 100644 --- a/resources/resources.qrc +++ b/resources/resources.qrc @@ -13,7 +13,7 @@ qml/Ui/TabView.qml qml/Ui/TagLabel.qml qml/Ui/ApplicationTextEdit.qml - qml/Ui/TextInput.qml + qml/Ui/UiTextInput.qml qml/Ui/CodeEditor.qml qml/Ui/CodeEditorError.qml qml/Ui/CodeEditorFooter.qml @@ -23,15 +23,35 @@ qml/Ui/ApplicationMenu.qml qml/Ui/ApplicationStatusBar.qml qml/Ui/ApplicationLogger.qml - qml/Ui/TelimetryPopup.qml + qml/Ui/TelemetryPopup.qml images/mur_logo256.png qml/Ui/AboutPopup.qml qml/Ui/RemoteView.qml - qml/Ui/GamepadSettings.qml + qml/Ui/SettingsGamepad.qml qml/Ui/AutoCompliter.qml images/video_placeholder.png help/help.html - images/mur_ide_elements.png qml/Ui/UpdatePopup.qml + qml/Ui/Style.qml + qml/Ui/qmldir + qml/Ui/NotificationPopup.qml + qml/Ui/Notifications.qml + qml/Ui/RemoteCamera.qml + qml/Ui/RovPanel.qml + qml/Ui/RovServoControl.qml + qml/Ui/SettingsPanel.qml + qml/Ui/UiTextEdit.qml + qml/Ui/Compass.qml + qml/Ui/TelemetryGroup.qml + fonts/NotoSans-Bold.ttf + fonts/NotoSans-Regular.ttf + fonts/NotoMono-Regular.ttf + qml/Ui/GamepadWidget.qml + qml/Ui/SettingsRemote.qml + qml/Ui/SettingsNetwork.qml + qml/Ui/RemoteKeyboardControl.qml + qml/Ui/Altimeter.qml + qml/Ui/BatterySettings.qml + images/mur_ide_elements_new.jpg diff --git a/sources/ApiTokenDialog.cpp b/sources/ApiTokenDialog.cpp index 133d4e7..aa6485a 100644 --- a/sources/ApiTokenDialog.cpp +++ b/sources/ApiTokenDialog.cpp @@ -1,4 +1,4 @@ -#include "APITokenDialog.hxx" +#include "ApiTokenDialog.hxx" #include #include #include @@ -47,7 +47,7 @@ void ApiTokenDialog::createConntetions() connect(m_applyButton.data(), &QPushButton::clicked, [this]{ m_token = m_tokenEdit->text().trimmed(); if (m_token.size() != 32) { - QMessageBox::warning(nullptr, "API Token size" ,"API Token must be 32 cheracter.", "Ok"); + QMessageBox::warning(nullptr, "API Token size" ,"API Token must be 32 characters.", "Ok"); return; } diff --git a/sources/Application.cpp b/sources/Application.cpp index 73f84cc..c100887 100644 --- a/sources/Application.cpp +++ b/sources/Application.cpp @@ -12,7 +12,8 @@ #include #include #include -#include +#include +#include namespace Ide::Ui { @@ -21,17 +22,15 @@ QString Application::m_resourceDirectory = {}; int Application::execute(int argc, char **argv) { - QtWebEngine::initialize(); QApplication app(argc, argv); + QtWebEngineQuick::initialize(); if (instance == nullptr) { instance = new Application; Ide::Ui::Application::initialize(); return QApplication::exec(); - } - - /* Something really bad happened */ + } return 0xDEADBEAF; } @@ -48,9 +47,9 @@ void Application::initialize() if (c.unicode() > 127) { QMessageBox::critical(nullptr, "Error (Ошибка)", - "Looks like the application path contains non ACII characters. " + "Looks like the application path contains non ASCII characters. " "Please move it to another directory or reinstall it\n" - "Путь к приложению содержит символы отличные от ACII (русские " + "Путь к приложению содержит символы отличные от ASCII (русские " "буквы). Переместите приложение или переустановите его.", "OK"); exit(0xDEADBEAF); @@ -68,6 +67,19 @@ void Application::initialize() addFontDirectory(); + QQuickStyle::setStyle("Fusion"); + QPalette p; + p = qApp->palette(); + p.setColor(QPalette::Window, "#282C34"); + p.setColor(QPalette::Button, "#363C46"); + p.setColor(QPalette::Highlight, "#776DAFF2"); + p.setColor(QPalette::ButtonText, "#FFFFFF"); + p.setColor(QPalette::Text, "#FFFFFF"); + p.setColor(QPalette::WindowText, "#BEBEBE"); + p.setColor(QPalette::ToolTipBase, "#282C34"); + p.setColor(QPalette::ToolTipText, "#BEBEBE"); + qApp->setPalette(p); + engine->addImportPath("qrc:/qml"); engine->addImportPath("qrc:/qml/Ui"); engine->addImportPath("qrc:/qml/UiElements"); @@ -76,6 +88,7 @@ void Application::initialize() auto root_object = engine->rootObjects().at(0); auto window = dynamic_cast(root_object); + window->setTitle("MUR IDE " + Ide::Ui::ApplicationController::getVersion()); QSurfaceFormat format; format.setMajorVersion(3); @@ -107,5 +120,8 @@ void Application::setupEnvironment() void Application::addFontDirectory() { QFontDatabase::addApplicationFont(":/fonts/fontawesome-webfont.ttf"); + QFontDatabase::addApplicationFont(":/fonts/NotoSans-Regular.ttf"); + QFontDatabase::addApplicationFont(":/fonts/NotoSans-Bold.ttf"); + QFontDatabase::addApplicationFont(":/fonts/NotoMono-Regular.ttf"); } -} // namespace Ide::Ui +} diff --git a/sources/ApplicationController.cpp b/sources/ApplicationController.cpp index e39a30a..abe49f6 100644 --- a/sources/ApplicationController.cpp +++ b/sources/ApplicationController.cpp @@ -1,12 +1,4 @@ #include "ApplicationController.hxx" -#include "ApplicationLogger.hxx" -#include "ApplicationMenu.hxx" -#include "EditorController.hxx" -#include "RemoteController.hxx" -#include "LocalScriptsController.hxx" -#include "NetworkController.hxx" -#include "SimulatorController.hxx" -#include "UpdateController.hxx" namespace Ide::Ui { @@ -60,10 +52,22 @@ UpdateController *ApplicationController::getUpdates() return UpdateController::instance; } +Joystick *ApplicationController::getJoystick() +{ + return Joystick::instance; +} + ApplicationController *ApplicationController::Create() { instance = new ApplicationController(); return instance; } -} //ui +bool ApplicationController::developerMode() +{ + QSettings settings("settings.ini", QSettings::IniFormat); + bool mode = settings.value("DevMode/active", false).toBool(); + return mode; +} + +} diff --git a/sources/ApplicationController.hxx b/sources/ApplicationController.hxx index 1fde5d2..66c8271 100644 --- a/sources/ApplicationController.hxx +++ b/sources/ApplicationController.hxx @@ -1,30 +1,33 @@ #pragma once #include "QmlUtils.hxx" #include "EditorController.hxx" +#include "ApplicationLogger.hxx" +#include "ApplicationMenu.hxx" +#include "RemoteController.hxx" +#include "LocalScriptsController.hxx" +#include "NetworkController.hxx" +#include "SimulatorController.hxx" +#include "UpdateController.hxx" +#include "Joystick.hxx" #include +#include namespace Ide::Ui { -class EditorController; -class ApplicationMenu; -class NetworkController; -class SimulatorController; -class ApplicationLogger; -class LocalScriptsController; -class RemoteController; -class UpdateController; - class ApplicationController : public QObject { Q_OBJECT - Q_PROPERTY(Ide::Ui::EditorController *editor READ getEditor CONSTANT) - Q_PROPERTY(Ide::Ui::ApplicationMenu *menu READ getMenu CONSTANT) - Q_PROPERTY(Ide::Ui::NetworkController *network READ getNetwork CONSTANT) - Q_PROPERTY(Ide::Ui::SimulatorController *simulator READ getSimulator CONSTANT) - Q_PROPERTY(Ide::Ui::ApplicationLogger *logger READ getLogger CONSTANT) - Q_PROPERTY(Ide::Ui::LocalScriptsController *scripts READ getScripts CONSTANT) - Q_PROPERTY(Ide::Ui::RemoteController *image READ getImage CONSTANT) - Q_PROPERTY(Ide::Ui::UpdateController *updates READ getUpdates CONSTANT) + Q_PROPERTY(EditorController *editor READ getEditor CONSTANT) + Q_PROPERTY(ApplicationMenu *menu READ getMenu CONSTANT) + Q_PROPERTY(NetworkController *network READ getNetwork CONSTANT) + Q_PROPERTY(SimulatorController *simulator READ getSimulator CONSTANT) + Q_PROPERTY(ApplicationLogger *logger READ getLogger CONSTANT) + Q_PROPERTY(LocalScriptsController *scripts READ getScripts CONSTANT) + Q_PROPERTY(RemoteController *image READ getImage CONSTANT) + Q_PROPERTY(UpdateController *updates READ getUpdates CONSTANT) + Q_PROPERTY(Ide::Ui::Joystick *joystick READ getJoystick CONSTANT) + Q_PROPERTY(QString version READ getVersion CONSTANT) + Q_PROPERTY(bool devMode READ developerMode CONSTANT) public: EditorController *getEditor(); @@ -35,6 +38,12 @@ public: LocalScriptsController *getScripts(); RemoteController *getImage(); UpdateController *getUpdates(); + Joystick *getJoystick(); + + static bool developerMode(); + static QString getVersion() { + return "0.1.0"; + } static ApplicationController *instance; static ApplicationController *Create(); @@ -43,4 +52,4 @@ private: ApplicationController(); static qml::RegisterType Register; }; -} // namespace ide::ui +} diff --git a/sources/ApplicationLogger.cpp b/sources/ApplicationLogger.cpp index f5e3823..e86f98a 100644 --- a/sources/ApplicationLogger.cpp +++ b/sources/ApplicationLogger.cpp @@ -12,21 +12,30 @@ ApplicationLogger *ApplicationLogger::Create() void ApplicationLogger::addEntry(const QString &string) { - if (m_entries.size() > 4500) { - clear(); + m_output += "
" + string + "
"; + emit outputChanged(); +} + +void ApplicationLogger::addOutput(const QString &output, const QString &error) +{ + m_output += output; + m_output += "
"; + if (!error.isEmpty()) { + m_output += "" + error + "
"; } - m_entries.append(string); } -QStringList ApplicationLogger::getEntries() +QString ApplicationLogger::getOutput() { - return m_entries; + static const QString header = ""; + static const QString footer = ""; + return header + m_output + footer; } void ApplicationLogger::clear() { - m_entries.clear(); - emit entriesChanged(); + m_output.clear(); + emit outputChanged(); } ApplicationLogger::ApplicationLogger() @@ -36,16 +45,17 @@ ApplicationLogger::ApplicationLogger() } m_updateViewTimer = new QTimer; connect(m_updateViewTimer, &QTimer::timeout, this, &ApplicationLogger::onUpdate); - m_updateViewTimer->start(150); + m_updateViewTimer->start(200); } void ApplicationLogger::onUpdate() { - static int entries_last_size = 0; - if (entries_last_size != m_entries.size()) { - emit entriesChanged(); - entries_last_size = m_entries.size(); + static long unsigned last_output_size = 0; + if (last_output_size != m_output.length()) { + m_output = m_output.right(20000); + last_output_size = m_output.length(); + emit outputChanged(); } } -} // namespace ide::ui +} diff --git a/sources/ApplicationLogger.hxx b/sources/ApplicationLogger.hxx index 222a2c5..e9a95ac 100644 --- a/sources/ApplicationLogger.hxx +++ b/sources/ApplicationLogger.hxx @@ -9,27 +9,29 @@ namespace Ide::Ui { class ApplicationLogger : public QObject { Q_OBJECT - Q_PROPERTY(QStringList entries READ getEntries NOTIFY entriesChanged) + Q_PROPERTY(QString output READ getOutput NOTIFY outputChanged) public: static ApplicationLogger *instance; static ApplicationLogger *Create(); void addEntry(const QString &); - QStringList getEntries(); + void addOutput(const QString &, const QString &); + QString getOutput(); public slots: void clear(); signals: - void entriesChanged(); + void outputReceived(const QString &, const QString &); + void outputChanged(); private: ApplicationLogger(); void onUpdate(); static qml::RegisterType Register; - QStringList m_entries; + QString m_output; QTimer *m_updateViewTimer; }; -} // namespace ide::ui +} diff --git a/sources/ApplicationMenu.cpp b/sources/ApplicationMenu.cpp index 90b0a9e..a6ae447 100644 --- a/sources/ApplicationMenu.cpp +++ b/sources/ApplicationMenu.cpp @@ -11,6 +11,7 @@ #include #include +#include namespace Ide::Ui { @@ -73,7 +74,7 @@ void ApplicationMenu::onCodeRun() LocalScriptsController::instance->run(); } else if (NetworkController::instance->getConnectionStatus()) { NetworkController::instance->run(); - ApplicationLogger::instance->addEntry("Program started."); + ApplicationLogger::instance->addEntry("
- Program started.
"); } } @@ -81,7 +82,7 @@ void ApplicationMenu::onCodeStop() { if (LocalScriptsController::instance->isLocal()) { LocalScriptsController::instance->stop(); - ApplicationLogger::instance->addEntry("Program stopped."); + ApplicationLogger::instance->addEntry("- Program stopped."); } else if (NetworkController::instance->getConnectionStatus()) { NetworkController::instance->stop(); } @@ -94,13 +95,6 @@ void ApplicationMenu::onRunSimulator() void ApplicationMenu::onRunRemote() { - /* - if (!image_porvider_controller::instance->is_reading_images()) { - image_porvider_controller::instance->start_image_capture(); - } else { - image_porvider_controller::instance->stop_image_capture(); - } */ - if (LocalScriptsController::instance->isLocal()) { return; } @@ -143,34 +137,18 @@ void ApplicationMenu::onViewToggleEditor() EditorController::instance->toggleExpanded(); } -void ApplicationMenu::onHelpAbout() -{ - // -} - -void ApplicationMenu::onHelpDocumentation() -{ - // -} - -void ApplicationMenu::onHelpPreferences() -{ - // -} - -void ApplicationMenu::onHelpVisitOnGitHub() +void ApplicationMenu::onHelpExample(const QString &exampleName) { - QDesktopServices::openUrl(QUrl("https://github.com/", QUrl::TolerantMode)); + EditorController::instance->openFile(Application::getResourcesDirectory() + "examples/" + exampleName); } -void ApplicationMenu::onHelpSendFeedback() -{ - QDesktopServices::openUrl( - QUrl("mailto:vlad@murproject.com?subject=IDE Feedback", QUrl::TolerantMode)); -} +void ApplicationMenu::onRestart() { + QProcess::startDetached(QApplication::applicationFilePath()); -void ApplicationMenu::onHelpExample(const QString &exampleName) -{ - EditorController::instance->openFile(Application::getResourcesDirectory() + "examples/" + exampleName); + if (QSysInfo::kernelType() == "winnt") { + qApp->quit(); + } else { + std::exit(EXIT_SUCCESS); + } } } diff --git a/sources/ApplicationMenu.hxx b/sources/ApplicationMenu.hxx index d67f86e..7efd455 100644 --- a/sources/ApplicationMenu.hxx +++ b/sources/ApplicationMenu.hxx @@ -34,12 +34,8 @@ public: void onViewResetFontSize(); void onViewToggleEditor(); - void onHelpDocumentation(); - void onHelpAbout(); - void onHelpPreferences(); - void onHelpVisitOnGitHub(); - void onHelpSendFeedback(); void onHelpExample(const QString &); + void onRestart(); private: QStringList m_examples; diff --git a/sources/EditorController.cpp b/sources/EditorController.cpp index 3f9b37a..9ed1b82 100644 --- a/sources/EditorController.cpp +++ b/sources/EditorController.cpp @@ -1,13 +1,6 @@ #include "EditorController.hxx" -#include "EditorErrors.hxx" -#include "EditorHighlighter.hxx" -#include "EditorIndenter.hxx" -#include "EditorHints.hxx" -#include "EditorSearch.hxx" -#include "EditorSelection.hxx" #include "EditorUtils.hxx" #include "TextIO.hxx" - #include #include #include @@ -40,7 +33,7 @@ void EditorController::setDocument(QQuickTextDocument *document) m_document = document; auto textDocument = document->textDocument(); auto options = textDocument->defaultTextOption(); - options.setTabStop(20); + options.setTabStopDistance(20); textDocument->setDefaultTextOption(options); m_highlighter = new EditorHighlighter(textDocument); @@ -274,4 +267,4 @@ EditorController::~EditorController() delete m_highlighter; m_highlighter = nullptr; } -} // namespace ide::ui +} diff --git a/sources/EditorController.hxx b/sources/EditorController.hxx index 3bae474..7410004 100644 --- a/sources/EditorController.hxx +++ b/sources/EditorController.hxx @@ -1,20 +1,18 @@ #pragma once #include "QmlUtils.hxx" +#include "EditorErrors.hxx" +#include "EditorHighlighter.hxx" +#include "EditorIndenter.hxx" +#include "EditorHints.hxx" +#include "EditorSearch.hxx" +#include "EditorSelection.hxx" #include #include namespace Ide::Ui { -class EditorUtils; -class EditorSearch; -class EditorErrors; -class editorSelection; -class EditorHighlighter; -class EditorIndenter; -class EditorHints; - class EditorController : public QObject { Q_OBJECT @@ -27,11 +25,11 @@ class EditorController : public QObject Q_PROPERTY(QString fileUrl READ getFileUrl NOTIFY fileUrlChanged) Q_PROPERTY(QString fileName READ getFileName NOTIFY fileNameChanged) Q_PROPERTY(QQuickTextDocument *document READ getDocument WRITE setDocument NOTIFY documentChanged) - Q_PROPERTY(Ide::Ui::EditorErrors *errors READ getErrors CONSTANT) - Q_PROPERTY(Ide::Ui::EditorSearch *search READ getSearch CONSTANT) - Q_PROPERTY(Ide::Ui::editorSelection *selection READ getSelection CONSTANT) - Q_PROPERTY(Ide::Ui::EditorIndenter *indenter READ getIndenter CONSTANT) - Q_PROPERTY(Ide::Ui::EditorHints *hints READ getHints CONSTANT) + Q_PROPERTY(EditorErrors *errors READ getErrors CONSTANT) + Q_PROPERTY(EditorSearch *search READ getSearch CONSTANT) + Q_PROPERTY(editorSelection *selection READ getSelection CONSTANT) + Q_PROPERTY(EditorIndenter *indenter READ getIndenter CONSTANT) + Q_PROPERTY(EditorHints *hints READ getHints CONSTANT) public: friend class Util; static EditorController *instance; @@ -112,4 +110,4 @@ private: void observedFileChanged(const QString &); void observeFile(const QString &); }; -} // namespace ide::ui +} diff --git a/sources/EditorHighlighter.cpp b/sources/EditorHighlighter.cpp index 6cc1501..b2be61f 100644 --- a/sources/EditorHighlighter.cpp +++ b/sources/EditorHighlighter.cpp @@ -25,33 +25,27 @@ void EditorHighlighter::init() { for (auto i = 0; i < ruleObjects.count(); ++i) { ruleObject = ruleObjects.at(i).toObject(); - if (ruleObject[Key::bold].toBool(false)) { - format.setFontWeight(QFont::Bold); - } else { - format.setFontWeight(QFont::Normal); - } + format.setFontWeight(QFont::Normal); format.setFontItalic(ruleObject[Key::italic].toBool(false)); format.setForeground(QColor(ruleObject[Key::color].toString("#ffffff"))); - rule.pattern = QRegExp(ruleObject[Key::match].toString()); + rule.pattern = QRegularExpression(ruleObject[Key::match].toString()); rule.format = format; m_rules.append(rule); } } + void EditorHighlighter::highlightBlock(const QString &text) { - for (const auto &rule : this->m_rules) { - auto expression = QRegExp(rule.pattern); - auto index = expression.indexIn(text); - - while (index >= 0) { - auto length = expression.matchedLength(); - setFormat(index, length, rule.format); - index = expression.indexIn(text, index + length); + for (const auto &rule : qAsConst(this->m_rules)) { + QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text); + while (matchIterator.hasNext()) { + QRegularExpressionMatch match = matchIterator.next(); + setFormat(match.capturedStart(), match.capturedLength(), rule.format); + } } - } } } diff --git a/sources/EditorHighlighter.hxx b/sources/EditorHighlighter.hxx index 05605b1..e15e2b3 100644 --- a/sources/EditorHighlighter.hxx +++ b/sources/EditorHighlighter.hxx @@ -2,6 +2,7 @@ #include #include +#include namespace Ide::Ui { @@ -17,7 +18,7 @@ private: static constexpr auto bold = "bold"; }; struct Rule { - QRegExp pattern; + QRegularExpression pattern; QTextCharFormat format; }; QList m_rules; diff --git a/sources/EditorHints.cpp b/sources/EditorHints.cpp index be07092..fdcd5b6 100644 --- a/sources/EditorHints.cpp +++ b/sources/EditorHints.cpp @@ -135,4 +135,4 @@ void EditorHints::declareQML() { qmlRegisterType("Hints", 1, 0, "Hints"); } -} // namespace Ide::Ui +} diff --git a/sources/EditorHints.hxx b/sources/EditorHints.hxx index adb1af3..d5891e1 100644 --- a/sources/EditorHints.hxx +++ b/sources/EditorHints.hxx @@ -15,7 +15,6 @@ class EditorHints : public QObject { Q_OBJECT public: - //TODO(Alexey): Make enum's names PascalCased enum HintType { HINT_COMMENT, HINT_INSERT_INDENT, HINT_REMOVE_INDENT }; Q_ENUMS(HintType) @@ -37,4 +36,4 @@ private: QTextDocument *m_textDocument = nullptr; }; -} // namespace ide::ui +} diff --git a/sources/EditorIndenter.cpp b/sources/EditorIndenter.cpp index 8d03f5d..3beabb6 100644 --- a/sources/EditorIndenter.cpp +++ b/sources/EditorIndenter.cpp @@ -46,7 +46,7 @@ static std::tuple>, QPair, bool, int> parseTex int lastColonLine = -1; bool shouldHang = false; - auto lines = text.split(QRegExp("[\r\n]"), QString::KeepEmptyParts); + auto lines = text.split(QRegularExpression("[\r\n]"), Qt::KeepEmptyParts); for (auto line = 0; line < lines.size(); ++line) { auto currentLine = lines.at(line); @@ -269,4 +269,4 @@ void EditorIndenter::computeIndent() QTextCursor cursor(m_textDocument->findBlockByLineNumber(line)); } -} // namespace Ide::Ui +} diff --git a/sources/EditorIndenter.hxx b/sources/EditorIndenter.hxx index ca6fd88..8ecc5f0 100644 --- a/sources/EditorIndenter.hxx +++ b/sources/EditorIndenter.hxx @@ -55,4 +55,4 @@ private: QTextDocument *m_textDocument = nullptr; }; -} // namespace ide::ui +} diff --git a/sources/EditorSearch.cpp b/sources/EditorSearch.cpp index c716568..ea9fe17 100644 --- a/sources/EditorSearch.cpp +++ b/sources/EditorSearch.cpp @@ -59,7 +59,24 @@ void EditorSearch::replaceNext() findNext(); } -void EditorSearch::replaceAll() {} +void EditorSearch::replaceAll() { + if (!m_valid) { + processSearch(); + return; + } + + if (m_matches.count() < 1) { + return; + } + + for(int i = 0; i < m_matches.count(); i ++) + { + auto cursor = m_matches.at(m_currentMatch); + cursor.insertText(m_replaceString); + findNext(); + } + processSearch(); +} void EditorSearch::setRegexError(const QString ®ex_error) { @@ -191,6 +208,16 @@ void EditorSearch::invalidate() m_valid = false; } +int EditorSearch::getStartPosition() +{ + return m_startPosition; +} + +int EditorSearch::getEndPosition() +{ + return m_endPosition; +} + void EditorSearch::processSearch() { if (m_useRegex && !m_regexValid) { @@ -227,6 +254,7 @@ void EditorSearch::processSearch() } m_valid = true; + emit matchCountChanged(); findNext(); @@ -242,10 +270,11 @@ void EditorSearch::findNext() if (m_matches.count() < 1) { return; } - setCurrentMatch((m_currentMatch + 1) % m_matches.count()); auto match = m_matches.at(m_currentMatch); + m_startPosition = match.selectionStart(); + m_endPosition = match.selectionEnd(); emit EditorController::instance->select(match.selectionStart(), match.selectionEnd()); } diff --git a/sources/EditorSearch.hxx b/sources/EditorSearch.hxx index d3e0e85..5486056 100644 --- a/sources/EditorSearch.hxx +++ b/sources/EditorSearch.hxx @@ -16,6 +16,8 @@ class EditorSearch : public QObject Q_PROPERTY(QString regexError READ getRegexError NOTIFY regexErrorChanged) Q_PROPERTY(int currentMatch READ getCurrentMatch NOTIFY currentMatchChanged) Q_PROPERTY(int matchCount READ getMatchCount NOTIFY matchCountChanged) + Q_PROPERTY(int startPosition READ getStartPosition NOTIFY startPositionChanged) + Q_PROPERTY(int endPosition READ getEndPosition NOTIFY endPositionChanged) Q_PROPERTY(bool caseSensitive READ getCaseSensitive WRITE setCaseSensitive NOTIFY caseSensitiveChanged) Q_PROPERTY(bool useRegex READ getUseRegex WRITE setUseRegex NOTIFY useRegexChanged) Q_PROPERTY(bool regexValid READ getRegexValid NOTIFY regexValidChanged) @@ -45,6 +47,8 @@ public: void setCurrentMatch(int); int getCurrentMatch(); int getMatchCount(); + int getStartPosition(); + int getEndPosition(); void invalidate(); public slots: @@ -62,6 +66,8 @@ signals: void caseSensitiveChanged(); void replaceStringChanged(); void matchCountChanged(); + void startPositionChanged(); + void endPositionChanged(); private: bool m_visible = false; @@ -75,6 +81,8 @@ private: bool m_useRegex = false; bool m_valid = false; int m_currentMatch = -1; + int m_startPosition = 0; + int m_endPosition = 0; EditorSearch(); }; } diff --git a/sources/Gamepad.cpp b/sources/Gamepad.cpp deleted file mode 100644 index 5ba37e0..0000000 --- a/sources/Gamepad.cpp +++ /dev/null @@ -1,267 +0,0 @@ -#include "Gamepad.hxx" -#include -#include - -namespace Ide::Ui { -Gamepad *Gamepad::instance = nullptr; -qml::RegisterType Gamepad::Register; - -Gamepad::Gamepad() -{ - if (instance != nullptr) { - throw std::runtime_error{"Instance of gamepad already exists"}; - } - m_gamepad = new QGamepad{}; - loadSettings(); - - m_gamepadAxesNames[gamepadAxes::axisLeftX] = "Left axis X"; - m_gamepadAxesNames[gamepadAxes::axisLeftY] = "Left axis Y"; - m_gamepadAxesNames[gamepadAxes::axisRightX] = "Right axis X"; - m_gamepadAxesNames[gamepadAxes::axisRightY] = "Right axis Y"; - - connect(m_gamepad, &QGamepad::axisLeftXChanged, this, &Gamepad::onLeftXChanged); - connect(m_gamepad, &QGamepad::axisLeftYChanged, this, &Gamepad::onLeftYChanged); - connect(m_gamepad, &QGamepad::axisRightXChanged, this, &Gamepad::onRightXChanged); - connect(m_gamepad, &QGamepad::axisRightYChanged, this, &Gamepad::onRightYChanged); -} - -void Gamepad::update() -{ - m_gamepadValues[gamepadAxes::axisLeftX] = m_gamepad->axisLeftX(); - m_gamepadValues[gamepadAxes::axisLeftY] = m_gamepad->axisLeftY(); - m_gamepadValues[gamepadAxes::axisRightX] = m_gamepad->axisRightX(); - m_gamepadValues[gamepadAxes::axisRightY] = m_gamepad->axisRightY(); -} - -void Gamepad::onLeftXChanged() -{ - emit axesValueChanged(); - if (!isRebindRequested()) { - return; - } - if (std::abs(m_gamepad->axisLeftX()) < 0.6) { - return; - } - rebind(gamepadAxes::axisLeftX); - emit axisNameChanged(); -} - -void Gamepad::onLeftYChanged() -{ - emit axesValueChanged(); - if (!isRebindRequested()) { - return; - } - if (std::abs(m_gamepad->axisLeftY()) < 0.6) { - return; - } - rebind(gamepadAxes::axisLeftY); - emit axisNameChanged(); -} - -void Gamepad::onRightXChanged() -{ - emit axesValueChanged(); - if (!isRebindRequested()) { - return; - } - if (std::abs(m_gamepad->axisRightX()) < 0.6) { - return; - } - rebind(gamepadAxes::axisRightX); - emit axisNameChanged(); -} - -void Gamepad::onRightYChanged() -{ - emit axesValueChanged(); - if (!isRebindRequested()) { - return; - } - if (std::abs(m_gamepad->axisRightY()) < 0.6) { - return; - } - rebind(gamepadAxes::axisRightY); - emit axisNameChanged(); -} - -void Gamepad::rebind(Gamepad::gamepadAxes axes) -{ - if (!isRebindRequested()) { - return; - } - if (m_rebintXrequested) { - m_gamepadBinding[powerAxes::axisX] = axes; - m_rebintXrequested = false; - } - - if (m_rebintYrequested) { - m_gamepadBinding[powerAxes::axisY] = axes; - m_rebintYrequested = false; - } - - if (m_rebintZrequested) { - m_gamepadBinding[powerAxes::axisZ] = axes; - m_rebintZrequested = false; - } - saveSettings(); -} - -bool Gamepad::isRebindRequested() -{ - return m_rebintXrequested || m_rebintYrequested || m_rebintZrequested; -} - -Gamepad *Gamepad::Create() -{ - instance = new Gamepad{}; - return instance; -} - -QGamepad *Gamepad::getGamepad() -{ - return m_gamepad; -} - -int Gamepad::getAxisXvalue() -{ - update(); - auto val = static_cast((m_gamepadValues[m_gamepadBinding[powerAxes::axisX]] - * (isInverseX() ? -100.0 : 100.0))); - return val; -} - -int Gamepad::getAxisYvalue() -{ - update(); - auto val = static_cast((m_gamepadValues[m_gamepadBinding[powerAxes::axisY]] - * (isInverseY() ? -100.0 : 100.0))); - return val; -} - -int Gamepad::getAxisZvalue() -{ - update(); - auto val = static_cast((m_gamepadValues[m_gamepadBinding[powerAxes::axisZ]] - * (isInverseZ() ? -100.0 : 100.0))); - - return val; -} - -QString Gamepad::getAxisXname() -{ - return m_gamepadAxesNames[m_gamepadBinding[powerAxes::axisX]]; -} - -QString Gamepad::getAxisYname() -{ - return m_gamepadAxesNames[m_gamepadBinding[powerAxes::axisY]]; -} - -QString Gamepad::getAxisZname() -{ - return m_gamepadAxesNames[m_gamepadBinding[powerAxes::axisZ]]; -} - -void Gamepad::rebindAxisX() -{ - m_rebintXrequested = true; - emit axisNameChanged(); -} - -void Gamepad::rebindAxisY() -{ - m_rebintYrequested = true; - emit axisNameChanged(); -} - -void Gamepad::rebindAxisZ() -{ - m_rebintZrequested = true; - emit axisNameChanged(); -} - -bool Gamepad::isRebindX() -{ - return m_rebintXrequested; -} - -bool Gamepad::isRebindY() -{ - return m_rebintYrequested; -} - -bool Gamepad::isRebindZ() -{ - return m_rebintZrequested; -} - -bool Gamepad::isInverseX() -{ - return m_isXinverse; -} - -bool Gamepad::isInverseY() -{ - return m_isYinverse; -} - -bool Gamepad::isInverseZ() -{ - return m_isZinverse; -} - -void Gamepad::setInverseX(bool val) -{ - m_isXinverse = val; - emit inverseionChanged(); - saveSettings(); -} - -void Gamepad::setInverseY(bool val) -{ - m_isYinverse = val; - emit inverseionChanged(); - saveSettings(); -} - -void Gamepad::setInverseZ(bool val) -{ - m_isZinverse = val; - emit inverseionChanged(); - saveSettings(); -} - -void Gamepad::saveSettings() -{ - QSettings settings("settings.ini", QSettings::IniFormat); - settings.setValue("axisX", static_cast(m_gamepadBinding[powerAxes::axisX])); - settings.setValue("axisY", static_cast(m_gamepadBinding[powerAxes::axisY])); - settings.setValue("axisZ", static_cast(m_gamepadBinding[powerAxes::axisZ])); - - settings.setValue("axisXInv", m_isXinverse); - settings.setValue("axisYInv", m_isYinverse); - settings.setValue("axisZInv", m_isZinverse); -} - -void Gamepad::loadSettings() -{ - QSettings settings("settings.ini", QSettings::IniFormat); - m_gamepadBinding[powerAxes::axisX] = static_cast( - settings.value("axisX", 0).toInt()); - - m_gamepadBinding[powerAxes::axisY] = static_cast( - settings.value("axisY", 1).toInt()); - - m_gamepadBinding[powerAxes::axisZ] = static_cast( - settings.value("axisZ", 2).toInt()); - - m_isXinverse = settings.value("axisXInv", false).toBool(); - m_isYinverse = settings.value("axisYInv", false).toBool(); - m_isZinverse = settings.value("axisZInv", false).toBool(); - - emit inverseionChanged(); - emit axisNameChanged(); -} - -} // namespace ide::ui diff --git a/sources/Gamepad.hxx b/sources/Gamepad.hxx deleted file mode 100644 index 92f6353..0000000 --- a/sources/Gamepad.hxx +++ /dev/null @@ -1,102 +0,0 @@ -#pragma once - -#include -#include - -#include "QmlUtils.hxx" - -namespace Ide::Ui { -class Gamepad : public QObject -{ - Q_OBJECT - Q_PROPERTY(QGamepad *Gamepad READ getGamepad CONSTANT) - - Q_PROPERTY(QString axisXName READ getAxisXname NOTIFY axisNameChanged) - Q_PROPERTY(QString axisYName READ getAxisYname NOTIFY axisNameChanged) - Q_PROPERTY(QString axisZName READ getAxisZname NOTIFY axisNameChanged) - - Q_PROPERTY(bool rebindX READ isRebindX NOTIFY axisNameChanged) - Q_PROPERTY(bool rebindY READ isRebindY NOTIFY axisNameChanged) - Q_PROPERTY(bool rebindZ READ isRebindZ NOTIFY axisNameChanged) - - Q_PROPERTY(bool inverseX READ isInverseX WRITE setInverseX NOTIFY inverseionChanged) - Q_PROPERTY(bool inverseY READ isInverseY WRITE setInverseY NOTIFY inverseionChanged) - Q_PROPERTY(bool inverseZ READ isInverseZ WRITE setInverseZ NOTIFY inverseionChanged) - - Q_PROPERTY(int axisXValue READ getAxisXvalue NOTIFY axesValueChanged) - Q_PROPERTY(int axisYValue READ getAxisYvalue NOTIFY axesValueChanged) - Q_PROPERTY(int axisZValue READ getAxisZvalue NOTIFY axesValueChanged) - -public: - static Gamepad *instance; - static Gamepad *Create(); - - QGamepad *getGamepad(); - -public slots: - - int getAxisXvalue(); - int getAxisYvalue(); - int getAxisZvalue(); - - QString getAxisXname(); - QString getAxisYname(); - QString getAxisZname(); - - void rebindAxisX(); - void rebindAxisY(); - void rebindAxisZ(); - - bool isRebindX(); - bool isRebindY(); - bool isRebindZ(); - - bool isInverseX(); - bool isInverseY(); - bool isInverseZ(); - - void setInverseX(bool); - void setInverseY(bool); - void setInverseZ(bool); - - void saveSettings(); - void loadSettings(); - -signals: - void axisNameChanged(); - void inverseionChanged(); - void axesValueChanged(); - -private: - enum class gamepadAxes { axisLeftX, axisLeftY, axisRightX, axisRightY }; - - enum class powerAxes { axisX, axisY, axisZ }; - - Gamepad(); - void update(); - static qml::RegisterType Register; - - QGamepad *m_gamepad; - QMap m_gamepadValues; - QMap m_gamepadBinding; - QMap m_gamepadAxesNames; - - void onLeftXChanged(); - void onLeftYChanged(); - void onRightXChanged(); - void onRightYChanged(); - - void rebind(gamepadAxes); - - bool isRebindRequested(); - - bool m_rebintXrequested = false; - bool m_rebintYrequested = false; - bool m_rebintZrequested = false; - - bool m_isXinverse = false; - bool m_isYinverse = false; - bool m_isZinverse = false; -}; - -} // namespace ide::ui diff --git a/sources/HintBase.hxx b/sources/HintBase.hxx index b9be324..1e3f681 100644 --- a/sources/HintBase.hxx +++ b/sources/HintBase.hxx @@ -23,7 +23,6 @@ public: virtual void apply() = 0; protected: - //TODO(Alexey): Move getLinesCount into editorSelection int getLinesCount(); static constexpr int indentWidth = 4; @@ -31,5 +30,4 @@ protected: QTextDocument *m_textDocument = nullptr; }; -} -// HINTBASE_HXX +} \ No newline at end of file diff --git a/sources/Joystick.cpp b/sources/Joystick.cpp new file mode 100644 index 0000000..9895143 --- /dev/null +++ b/sources/Joystick.cpp @@ -0,0 +1,337 @@ +#include "Joystick.hxx" +#include + +namespace Ide::Ui{ +Joystick *Joystick::instance = nullptr; +qml::RegisterType Joystick::Register; + +QList JoystickAxes::JoystickAxesNames = { + "···", + "Left X", + "Left Y", + "Right X", + "Right Y", + "L2", + "R2", + "A", + "B", + "X", + "Y", + "L1", + "R1", + "Select", + "Start", + "L3", + "R3", + "Up", + "Right", + "Down", + "Left", + "Center", + "Guide", +}; + +QList JoystickAxes::MovementAxesNames = { + "Empty", + "Axis X (yaw)", + "Axis Y (forward)", + "Axis W (side)", + "Axis Z (depth)", + "L2", + "R2", + "Slower", + "Faster", + "Axis count", +}; + +Joystick::Joystick() { + + if (instance != nullptr) { + throw std::runtime_error{"Instance of gamepad already exists"}; + } + m_joystick = QJoysticks::getInstance(); + loadSettings(); + + qmlRegisterUncreatableType("mur.GamepadAxes", 1, 0, "GamepadAxes", ""); + + m_updateTimer = new QTimer{}; + m_updateTimer->setInterval(25); + connect(m_updateTimer, &QTimer::timeout, this, &Joystick::onUpdateTimeout); + m_updateTimer->start(); + + this->connectJoystick(); + connect(QJoysticks::getInstance(), &QJoysticks::axisChanged, this, &Joystick::onAxisEvent); + connect(QJoysticks::getInstance(), &QJoysticks::buttonChanged, this, &Joystick::onButtonEvent); + connect(QJoysticks::getInstance(), &QJoysticks::povChanged, this, &Joystick::onPOVEvent); + connect(QJoysticks::getInstance(), &QJoysticks::countChanged, this, &Joystick::connectJoystick); +} + +void Joystick::onUpdateTimeout() { + if (m_axisChanged) { + m_axisChanged = false; + emit axesValueChanged(); + } +} + +void Joystick::onAxisEvent(int device, int axis, double value){ + m_gamepadAllValues[axis + 1] = value; + + if (m_isRebinding && qAbs(value) > 0.6) { + rebindAxis(axis + 1); + } + + m_axisChanged = true; +} + +void Joystick::onButtonEvent(int device, int button, bool pressed){ + double value = (pressed ? 1 : -1); + m_gamepadAllValues[button + 7] = value; + + if (m_isRebinding && qAbs(value) > 0.6) { + rebindAxis(button + 7); + } + + emit axesValueChanged(); +} + +void Joystick::onPOVEvent(int device, int pov, int angle){ + auto value = angle / 90 + 1; + + if (angle > -1) { + m_gamepadAllValues[16 + value] = 1; + } else { + for(int i = povStartIndex; i < povEndIndex; i++){ + m_gamepadAllValues[i] = -1; + } + } + + if (m_isRebinding && qAbs(value) > 0.6) { + rebindAxis(16 + value); + } + + emit axesValueChanged(); +} + +int Joystick::getAxisValue(JoystickAxes::MovementAxes axis){ + return calcAxisValue(m_gamepadAllValues[m_gamepadAxesBindings[axis]] * (m_gamepadAxesInversions[axis] ? -100 : 100)); +} + +bool Joystick::getButtonValue(JoystickAxes::MovementAxes axis) { + return calcAxisValue(m_gamepadAllValues[m_gamepadAxesBindings[axis]] * (m_gamepadAxesInversions[axis] ? -100 : 100)) > 50; +} + +bool Joystick::getAxisInversion(JoystickAxes::MovementAxes axis) { + return m_gamepadAxesInversions[axis]; +} + +void Joystick::setAxisInversion(int axis, bool inversed) { + m_gamepadAxesInversions[static_cast(axis)] = inversed; + emit rebinded(); +} + +void Joystick::clearAxis(int axis) { + m_currentlyRebindingAxis = static_cast(axis); + setAxisInversion(axis, false); + rebindAxis(0); +} + +void Joystick::rebindAxis(int axis) { + m_gamepadAxesBindings[m_currentlyRebindingAxis] = axis; + m_currentlyRebindingAxis = JoystickAxes::AxisZero; + m_isRebinding = false; + emit rebindingChanged(); + emit rebinded(); +} + +void Joystick::swapAxes(int axis1, int axis2) { + auto gamepadAxis1 = m_gamepadAxesBindings[static_cast(axis1)]; + auto gamepadAxis2 = m_gamepadAxesBindings[static_cast(axis2)]; + + m_gamepadAxesBindings[static_cast(axis2)] = gamepadAxis1; + m_gamepadAxesBindings[static_cast(axis1)] = gamepadAxis2; + + m_isRebinding = false; + emit rebindingChanged(); + emit rebinded(); +} + +void Joystick::requestRebind(int axis) { + m_isRebinding = true; + m_currentlyRebindingAxis = static_cast(axis); + emit rebindingChanged(); +} + +QList Joystick::getAllAxes() { + QList axes; + for (int i = 0; i < JoystickAxes::AxisCount; i++) { + axes.append(getAxisValue(static_cast(i))); + } + return axes; +} + +QList Joystick::getAllAxesInversions() { + QList axes; + + for (int i = 0; i < JoystickAxes::AxisCount; i++) { + axes.append(getAxisInversion(static_cast(i))); + } + + return axes; +} + +QList Joystick::getAllAxesBindings() { + QList axes; + + for (int i = 0; i < JoystickAxes::AxisCount; i++) { + axes.append(getGamepadAxisName(i)); + } + + return axes; +} + +QString Joystick::getGamepadAxisName(int axis) +{ + return JoystickAxes::JoystickAxesNames[m_gamepadAxesBindings[static_cast(axis)]]; +} + +QString Joystick::getMovementAxisName(int axis) +{ + return JoystickAxes::MovementAxesNames[static_cast(axis)]; +} + +bool Joystick::isRebinding() { + return m_isRebinding; +} + +int Joystick::getRebindingAxis() { + return m_currentlyRebindingAxis; +} + +void Joystick::setForceAxisValue(int axis, int value) { + m_gamepadAllValues[axis] = value / 100.0f; + emit axesValueChanged(); +} + +void Joystick::addForceAxisValue(int axis, int value) { + m_gamepadAllValues[axis] = m_gamepadAllValues[axis] + (value / 100.0f); + emit axesValueChanged(); +} + +bool Joystick::isJoystickConnected() { + return m_joystick->joystickExists(0); +} + +Joystick *Joystick::Create(){ + instance = new Joystick{}; + return instance; +} + +QJoysticks *Joystick::getJoystick() +{ + return m_joystick; +} + +int Joystick::calcAxisValue(int val) { + if (abs(val) < m_deadzone) { + return 0; + } + + bool invert = val < 0; + + val = (abs(static_cast(val)) - m_deadzone) / (100.0f - m_deadzone) * 100.0f; + val = qPow(static_cast(val), m_expFactor) / qPow(100.0f, m_expFactor - 1.0f); + + val *= (invert ? -1 : 1); + val = qMax(qMin(val, 100), -100); + return val; +} + +int Joystick::getDeadzone() { + return m_deadzone; +} + +void Joystick::setDeadzone(int val) +{ + m_deadzone = val; + emit deadzoneChanged(); +} + +double Joystick::getExpFactor() { + return m_expFactor; +} + +void Joystick::setExpFactor(double value) { + m_expFactor = value; + emit expFactorChanged(); +} + +void Joystick::saveSettings() +{ + QSettings settings("settings.ini", QSettings::IniFormat); + + settings.beginWriteArray("Gamepad/axes"); + for (int i = 0; i < JoystickAxes::AxisCount; i++) { + settings.setArrayIndex(i); + settings.setValue("binding", m_gamepadAxesBindings[static_cast(i)]); + settings.setValue("inversion", m_gamepadAxesInversions[static_cast(i)]); + } + settings.endArray(); + + settings.setValue("Gamepad/deadzone", m_deadzone); + settings.setValue("Gamepad/exp_factor", m_expFactor); + settings.endArray(); +} + +void Joystick::loadSettings() +{ + QSettings settings("settings.ini", QSettings::IniFormat); + + int size = settings.beginReadArray("Gamepad/axes"); + for (int i = 0; i < size; i++) { + settings.setArrayIndex(i); + m_gamepadAxesBindings[static_cast(i)] = settings.value("binding", 0).toInt(); + m_gamepadAxesInversions[static_cast(i)] = settings.value("inversion", false).toBool(); + m_gamepadAxesBindings[JoystickAxes::AxisW] = 0; + } + settings.endArray(); + + if (m_gamepadAxesBindings[JoystickAxes::AxisX] == 0) { + m_gamepadAxesBindings[JoystickAxes::AxisX] = 1; + } + + if (m_gamepadAxesBindings[JoystickAxes::AxisY] == 0) { + m_gamepadAxesBindings[JoystickAxes::AxisY] = 2; + } + + if (m_gamepadAxesBindings[JoystickAxes::AxisZ] == 0) { + m_gamepadAxesBindings[JoystickAxes::AxisZ] = 4; + } + + if (m_gamepadAxesBindings[JoystickAxes::SpeedSlow] == 0) { + m_gamepadAxesBindings[JoystickAxes::SpeedSlow] = 7; + } + + if (m_gamepadAxesBindings[JoystickAxes::SpeedFast] == 0) { + m_gamepadAxesBindings[JoystickAxes::SpeedFast] = 8; + } + + m_deadzone = settings.value("Gamepad/deadzone", 0).toInt(); + m_expFactor = settings.value("Gamepad/exp_factor", 1.0).toDouble(); + + settings.endArray(); + + emit deadzoneChanged(); + emit expFactorChanged(); + emit rebinded(); +} + +void Joystick::connectJoystick() +{ + auto joy = QJoysticks::getInstance()->joystickExists(0); + + if (!qApp->closingDown()) { + emit onJoystickConnectedChanged(joy); + } +} +} + diff --git a/sources/Joystick.hxx b/sources/Joystick.hxx new file mode 100644 index 0000000..c9c09e6 --- /dev/null +++ b/sources/Joystick.hxx @@ -0,0 +1,138 @@ +#pragma once +#include +#include +#include "QmlUtils.hxx" +#include +#include + +namespace Ide::Ui +{ + +class JoystickAxes : public QObject +{ + Q_OBJECT + +public: + enum MovementAxes { + AxisZero, + + AxisX, + AxisY, + AxisW, + AxisZ, + + AxisYaw = AxisX, + AxisForward = AxisY, + AxisSide = AxisW, + AxisDepth = AxisZ, + L2, + R2, + + SpeedSlow, + SpeedFast, + + AxisCount, + }; + Q_ENUM(MovementAxes) + + static QList JoystickAxesNames; + static QList MovementAxesNames; +}; + +class Joystick : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool joystickConnected READ isJoystickConnected NOTIFY onJoystickConnectedChanged) + + Q_PROPERTY(QList allAxes READ getAllAxes NOTIFY axesValueChanged) + Q_PROPERTY(QList allAxesInversions READ getAllAxesInversions NOTIFY rebinded) + Q_PROPERTY(QList allAxesBindings READ getAllAxesBindings NOTIFY rebinded) + + Q_PROPERTY(bool isRebinding READ isRebinding NOTIFY rebindingChanged) + Q_PROPERTY(int rebindingAxis READ getRebindingAxis NOTIFY rebindingChanged) + + Q_PROPERTY(int deadzone READ getDeadzone WRITE setDeadzone NOTIFY deadzoneChanged) + Q_PROPERTY(double expFactor READ getExpFactor WRITE setExpFactor NOTIFY expFactorChanged) + + +public: + static Joystick *instance; + static Joystick *Create(); + + QJoysticks *getJoystick(); + bool isJoystickConnected(); + +public slots: + int getAxisValue(JoystickAxes::MovementAxes); + bool getButtonValue(JoystickAxes::MovementAxes); + + bool getAxisInversion(JoystickAxes::MovementAxes axis); + void setAxisInversion(int, bool); + + void clearAxis(int); + void requestRebind(int); + bool isRebinding(); + int getRebindingAxis(); + void swapAxes(int, int); + + QList getAllAxes(); + QList getAllAxesInversions(); + QList getAllAxesBindings(); + void setForceAxisValue(int, int); + void addForceAxisValue(int, int); + + QString getGamepadAxisName(int axis); + QString getMovementAxisName(int axis); + + int getDeadzone(); + void setDeadzone(int); + + double getExpFactor(); + void setExpFactor(double); + + void saveSettings(); + void loadSettings(); + + int calcAxisValue(int); + +signals: + void axesValueChanged(); + void onJoystickConnectedChanged(bool); + void deadzoneChanged(); + void rebindingChanged(); + void rebinded(); + void prorovFunctionsChanged(); + void expFactorChanged(); + +private: + Joystick(); + static qml::RegisterType Register; + + QJoysticks *m_joystick; + QMap m_gamepadAllValues; + + QMap m_gamepadAxesBindings; + QMap m_gamepadAxesInversions; + + void rebindAxis(int axis); + + bool m_isRebinding = false; + JoystickAxes::MovementAxes m_currentlyRebindingAxis; + + void onAxisEvent(int, int, double); + void onButtonEvent(int, int, bool); + void onPOVEvent(int, int, int); + + int m_deadzone = 0; + double m_expFactor = 1.0; + int povStartIndex = 17; + int povEndIndex = 21; + + QTimer* m_updateTimer; + void onUpdateTimeout(); + bool m_axisChanged = false; + + void connectJoystick(); +}; +} diff --git a/sources/JsonUtils.cpp b/sources/JsonUtils.cpp index b936212..268f320 100644 --- a/sources/JsonUtils.cpp +++ b/sources/JsonUtils.cpp @@ -1,6 +1,9 @@ #include "JsonUtils.hxx" #include #include +#include +#include +#include namespace Ide::IO { namespace FromJson { @@ -15,6 +18,16 @@ Ide::IO::Telemetry telemetry(const QString &json) return Ide::IO::Telemetry{}; } + if (object.contains("vehicle_type")) { + telemetry_local.vehicle_type = object["vehicle_type"].toString(); + } else { + telemetry_local.vehicle_type = "auv"; + } + + if (object.contains("vehicle_name")) { + telemetry_local.vehicle_name = object["vehicle_name"].toString(); + } + if (object.contains("vehicle_type") && object["vehicle_type"].toString() == "usv") { bool is_running = object["running"].toBool(); bool is_remote = object["remote"].toBool(); @@ -41,20 +54,40 @@ Ide::IO::Telemetry telemetry(const QString &json) telemetry_local.latitude = latitude; telemetry_local.speed = speed; telemetry_local.altitude = altitude; - telemetry_local.is_usv = true; return telemetry_local; } bool is_running = object["running"].toBool(); bool is_remote = object["remote"].toBool(); + bool is_charging = object["is_charging"].toBool(); + bool is_shell_running = object["is_shell_running"].toBool(); + bool is_motors_enabled = object["is_motors_enabled"].toBool(); double yaw = object["yaw"].toDouble(); double pitch = object["pitch"].toDouble(); double roll = object["roll"].toDouble(); double depth = object["depth"].toDouble(); double pressure = object["pressure"].toDouble(); + double temperature = object["temperature"].toDouble(); double battery = object["battery"].toDouble(); + int fg_cycle_count = object["fg_cycle_count"].toInt(); + int fg_full_charge_capacity = object["fg_full_charge_capacity"].toInt(); + int fg_max_error = object["fg_max_error"].toInt(); + int fg_remaining_capacity = object["fg_remaining_capacity"].toInt(); + int fg_reset_count = object["fg_reset_count"].toInt(); + int fg_update_status = object["fg_update_status"].toInt(); + + double fg_temp = object["fg_temp"].toDouble(); + double amperage = object["amperage"].toDouble(); + double voltage = object["voltage"].toDouble(); + + bool fg_flag_fc = object["fg_flag_fc"].toBool(); + bool fg_flag_qen = object["fg_flag_qen"].toBool(); + bool fg_flag_rup_dis = object["fg_flag_rup_dis"].toBool(); + bool fg_flag_vok_flag = object["fg_flag_vok_flag"].toBool(); + bool fg_flag_ocvtaken = object["fg_flag_ocvtaken"].toBool(); + telemetry_local.is_running= is_running; telemetry_local.is_remote = is_remote; telemetry_local.yaw = yaw; @@ -62,11 +95,91 @@ Ide::IO::Telemetry telemetry(const QString &json) telemetry_local.roll = roll; telemetry_local.depth = depth; telemetry_local.pressure = pressure; + telemetry_local.temperature = temperature; telemetry_local.battery = battery; + telemetry_local.is_charging = is_charging; + telemetry_local.is_shell_running = is_shell_running; + telemetry_local.is_motors_enabled = is_motors_enabled; + telemetry_local.fg_cycle_count = fg_cycle_count; + telemetry_local.fg_full_charge_capacity = fg_full_charge_capacity; + telemetry_local.fg_max_error = fg_max_error; + telemetry_local.fg_remaining_capacity = fg_remaining_capacity; + telemetry_local.fg_reset_count = fg_reset_count; + telemetry_local.fg_update_status = fg_update_status; + telemetry_local.fg_temp = fg_temp; + telemetry_local.amperage = amperage; + telemetry_local.voltage = voltage; + telemetry_local.fg_flag_fc = fg_flag_fc; + telemetry_local.fg_flag_qen = fg_flag_qen; + telemetry_local.fg_flag_rup_dis = fg_flag_rup_dis; + telemetry_local.fg_flag_vok_flag = fg_flag_vok_flag; + telemetry_local.fg_flag_ocvtaken = fg_flag_ocvtaken; + + if (object.contains("vehicle_type") && object["vehicle_type"].toString() == "rov") { + double fcu0_voltage = object["fcu"].toArray()[0].toObject()["voltage"].toDouble(); + double fcu0_amperage = object["fcu"].toArray()[0].toObject()["amperage"].toDouble(); + double fcu1_voltage = object["fcu"].toArray()[1].toObject()["voltage"].toDouble(); + double fcu1_amperage = object["fcu"].toArray()[1].toObject()["amperage"].toDouble(); + + telemetry_local.fcu0_voltage = fcu0_voltage; + telemetry_local.fcu0_amperage = fcu0_amperage; + telemetry_local.fcu1_voltage = fcu1_voltage; + telemetry_local.fcu1_amperage = fcu1_amperage; + } return telemetry_local; } +Ide::IO::Notification notification(const QString &json) +{ + Ide::IO::Notification notification; + + auto doc = QJsonDocument::fromJson(json.toUtf8()); + auto object = doc.object(); + + if (!(object.contains("type") && object["type"].toString() == "notification")) { + return Ide::IO::Notification{}; + } + + notification.status = object["status"].toString(); + notification.message = object["message"].toString(); + + return notification; +} + +QString diagnostic_log(const QString &json) +{ + auto doc = QJsonDocument::fromJson(json.toUtf8()); + auto object = doc.object(); + + if (!(object.contains("type") && object["type"].toString() == "diagnostic_log_response")) { + return ""; + } + + QString log = object["output"].toString(); + log.replace(QRegularExpression("\x1b\[[0-9;]*m"), ""); + + return log; +} + +QStringList software_features(const QString &json) +{ + auto doc = QJsonDocument::fromJson(json.toUtf8()); + auto object = doc.object(); + QStringList result; + + if (!(object.contains("type") && object["type"].toString() == "software_features")) { + return result; + } + + QJsonArray features = object["features"].toArray(); + + for (auto item : features) { + result << item.toString(); + } + + return result; +} QPair output(const QString &json) { auto doc = QJsonDocument::fromJson(json.toUtf8()); @@ -82,7 +195,7 @@ QPair output(const QString &json) return {out, err}; } -} // namespace FromJson +} namespace ToJson { @@ -100,12 +213,16 @@ QString code(const QString &filename, const QString &content) return str; } -QString remote() +QString remote(const QString &pipeline) { QJsonObject json; json["type"] = "remote"; + if (!pipeline.isEmpty()) { + json["pipeline"] = pipeline; + } + QJsonDocument doc(json); QString str(doc.toJson(QJsonDocument::Compact)); @@ -136,6 +253,57 @@ QString stop_remote() return str; } +QString diagnostic_log() +{ + QJsonObject json; + + json["type"] = "get_diagnostic_log"; + + QJsonDocument doc(json); + QString str(doc.toJson(QJsonDocument::Compact)); + + return str; +} + +QString software_features() +{ + QJsonObject json; + + json["type"] = "get_software_features"; + + QJsonDocument doc(json); + QString str(doc.toJson(QJsonDocument::Compact)); + + return str; +} + +QString shell_command_run(QString cmd, QStringList args) +{ + QJsonObject json; + + json["type"] = "shell_command_run"; + json["cmd"] = cmd; + json["args"] = QJsonArray::fromStringList(args); + + QJsonDocument doc(json); + QString str(doc.toJson(QJsonDocument::Compact)); + + return str; +} + +QString mur_upgrade() +{ + QJsonObject json; + + json["type"] = "mur_upgrade"; + json["proxy_port"] = "10001"; + + QJsonDocument doc(json); + QString str(doc.toJson(QJsonDocument::Compact)); + + return str; +} + QString api_token(const QByteArray& token) { QJsonObject json; @@ -149,6 +317,31 @@ QString api_token(const QByteArray& token) return str; } -} // namespace ToJson +QString set_time(const qint64 time) +{ + QJsonObject json; + + json["type"] = "set_time"; + json["time"] = QString::number(time); + + QJsonDocument doc(json); + QString str(doc.toJson(QJsonDocument::Compact)); + + return str; +} + +QString battery_command(QString command){ + QJsonObject json; + json["type"] = "battery_command"; + json["command"] = command; + + QJsonDocument doc(json); + QString str(doc.toJson(QJsonDocument::Compact)); + + return str; + +} + +} -} // namespace ide::io +} diff --git a/sources/JsonUtils.hxx b/sources/JsonUtils.hxx index f780a4e..8c3ab6e 100644 --- a/sources/JsonUtils.hxx +++ b/sources/JsonUtils.hxx @@ -3,44 +3,89 @@ #include #include #include +#include namespace Ide::IO { struct Telemetry { + QString vehicle_type; + QString vehicle_name; + QString software_version; + bool is_running = false; bool is_remote = false; bool is_mission = false; bool is_charging = false; - bool is_usv = false; + bool is_shell_running = false; + bool is_motors_enabled = false; double yaw = 0.0; double pitch = 0.0; double roll = 0.0; double depth = 0.0; double pressure = 0.0; + double temperature = 0.0; double battery = 0.0; double longitude = 0.0; double latitude = 0.0; double altitude = 0.0; double satellites = 0; double speed = 0.0; + + double fcu0_voltage = 0.0; + double fcu0_amperage = 0.0; + double fcu1_voltage = 0.0; + double fcu1_amperage = 0.0; + + + int fg_cycle_count = 0; + int fg_full_charge_capacity = 0; + int fg_max_error = 0; + int fg_remaining_capacity = 0; + int fg_reset_count = 0; + int fg_update_status = 0; + + double fg_temp = 0.0; + double amperage = 0.0; double voltage = 0.0; + bool fg_flag_fc = false; + bool fg_flag_qen = false; + bool fg_flag_rup_dis = false; + bool fg_flag_vok_flag = false; + bool fg_flag_ocvtaken = false; + + +}; + +struct Notification +{ + QString status; + QString message; }; namespace FromJson { Ide::IO::Telemetry telemetry(const QString &); +Ide::IO::Notification notification(const QString &); QPair output(const QString &); -} // namespace FromJson +QString diagnostic_log(const QString &); +QStringList software_features(const QString &); +} namespace ToJson { QString code(const QString &, const QString &); -QString remote(); +QString remote(const QString &pipeline = ""); QString stop_remote(); QString stop(); QString api_token(const QByteArray& token); +QString diagnostic_log(); +QString software_features(); +QString shell_command_run(QString cmd, QStringList args); +QString mur_upgrade(); +QString set_time(qint64); +QString battery_command(QString command); -} // namespace ToJson +} -} // namespace ide::io +} diff --git a/sources/LocalScriptsController.cpp b/sources/LocalScriptsController.cpp index 11008c7..dbfd2ac 100644 --- a/sources/LocalScriptsController.cpp +++ b/sources/LocalScriptsController.cpp @@ -56,7 +56,7 @@ void LocalScriptsController::stop() if (m_scriptProcess->state() == QProcess::NotRunning) { return; } - m_scriptProcess->terminate(); + m_scriptProcess->kill(); } bool LocalScriptsController::isRunning() @@ -107,9 +107,9 @@ void LocalScriptsController::setupProcess() &LocalScriptsController::runningStateChanged); connect(m_scriptProcess, - qOverload(&QProcess::finished), + qOverload(&QProcess::finished), this, &LocalScriptsController::runningStateChanged); } -} // namespace ide::ui +} diff --git a/sources/LocalScriptsController.hxx b/sources/LocalScriptsController.hxx index 4bcf8b1..48a4ecd 100644 --- a/sources/LocalScriptsController.hxx +++ b/sources/LocalScriptsController.hxx @@ -29,7 +29,7 @@ private: static qml::RegisterType Register; void setupProcess(); QProcess *m_scriptProcess = nullptr; - Q_PID m_pid; + qint64 m_pid; bool m_isLocal = false; }; -} // namespace ide::ui +} diff --git a/sources/LocalScriptsController_unix.cpp b/sources/LocalScriptsController_unix.cpp index 036eecf..0d0bc00 100644 --- a/sources/LocalScriptsController_unix.cpp +++ b/sources/LocalScriptsController_unix.cpp @@ -3,8 +3,6 @@ #include "ApplicationLogger.hxx" #include "EditorController.hxx" -// #include - namespace Ide::Ui { LocalScriptsController *LocalScriptsController::instance = nullptr; @@ -85,9 +83,9 @@ void LocalScriptsController::setupProcess() &LocalScriptsController::runningStateChanged); connect(m_scriptProcess, - qOverload(&QProcess::finished), + qOverload(&QProcess::finished), this, &LocalScriptsController::runningStateChanged); } -} // namespace ide::ui +} diff --git a/sources/LocalScriptsController_win32.cpp b/sources/LocalScriptsController_win32.cpp index 11008c7..9372af6 100644 --- a/sources/LocalScriptsController_win32.cpp +++ b/sources/LocalScriptsController_win32.cpp @@ -47,7 +47,7 @@ void LocalScriptsController::run() QStringList() << "/c" << py_path << script_path << "&" << "pause"); m_scriptProcess->waitForStarted(); - m_pid = m_scriptProcess->pid(); + m_pid = m_scriptProcess->processId(); ApplicationLogger::instance->addEntry("Program started."); } @@ -57,6 +57,7 @@ void LocalScriptsController::stop() return; } m_scriptProcess->terminate(); + m_scriptProcess->kill(); } bool LocalScriptsController::isRunning() @@ -107,9 +108,9 @@ void LocalScriptsController::setupProcess() &LocalScriptsController::runningStateChanged); connect(m_scriptProcess, - qOverload(&QProcess::finished), + qOverload(&QProcess::finished), this, &LocalScriptsController::runningStateChanged); } -} // namespace ide::ui +} diff --git a/sources/NetworkController.cpp b/sources/NetworkController.cpp index 1034099..9b74705 100644 --- a/sources/NetworkController.cpp +++ b/sources/NetworkController.cpp @@ -14,21 +14,27 @@ NetworkController::NetworkController() if (instance != nullptr) { throw std::runtime_error("Instance of network_controller already exists"); } + + QSettings settings("settings.ini", QSettings::IniFormat); + m_connectionAddress = settings.value("Network/connection_address", "10.3.141.1:2090").toString(); + m_reconnectTime = settings.value("Network/reconnect_time", 1000).toInt(); + m_pingTime = settings.value("Network/ping_time", 1000).toInt(); + m_pongTime = settings.value("Network/pong_time", 3000).toInt(); + m_webSocket = new QWebSocket{}; + m_reconnectionTimer = new QTimer{}; m_pingTimer = new QTimer{}; m_pongTimer = new QTimer{}; m_ApiTokenDialog = new ApiTokenDialog{}; - m_reconnectionTimer = new QTimer{}; - m_reconnectionTimer->start(3000); - - m_pingTimer->setInterval(5000); - m_pongTimer->setInterval(10000); + m_reconnectionTimer->start(m_reconnectTime); + m_pingTimer->setInterval(m_pingTime); + m_pongTimer->setInterval(m_pongTime); connect(m_reconnectionTimer, &QTimer::timeout, this, &NetworkController::onReconnectTimer); connect(m_webSocket, &QWebSocket::connected, this, &NetworkController::onConnected); connect(m_webSocket, &QWebSocket::disconnected, this, &NetworkController::onDisconnected); - connect(m_webSocket, &QWebSocket::textMessageReceived, this, &NetworkController::onTelimetryReceived); + connect(m_webSocket, &QWebSocket::textMessageReceived, this, &NetworkController::onTelemetryReceived); connect(m_webSocket, &QWebSocket::pong, this, &NetworkController::onPongReceived); connect(m_pongTimer, &QTimer::timeout, this, &NetworkController::onPongTimeout); connect(m_pingTimer, &QTimer::timeout, this, &NetworkController::onPingTimeout); @@ -39,6 +45,7 @@ void NetworkController::onConnected() { m_reconnectionTimer->stop(); m_pingTimer->start(); + requestFeatures(); emit connectionStatusChanged(); } @@ -54,44 +61,145 @@ void NetworkController::onDisconnected() void NetworkController::onReconnectTimer() { m_webSocket->abort(); - m_webSocket->open((QUrl("ws://10.3.141.1:2090"))); + m_webSocket->open((QUrl("ws://" + m_connectionAddress))); } -void NetworkController::onTelimetryReceived(const QString &message) -{ - auto doc = QJsonDocument::fromJson(message.toUtf8()); - auto object = doc.object(); +void NetworkController::requestFeatures() { + m_softwareFeatures.clear(); + m_webSocket->sendTextMessage(Ide::IO::ToJson::software_features()); +} - if ((object.contains("type") && object["type"].toString() == "output")) { - auto [error_b64, output_b64] = Ide::IO::FromJson::output(message); +void NetworkController::syncVehicleTime() { + m_webSocket->sendTextMessage(Ide::IO::ToJson::set_time(QDateTime::currentSecsSinceEpoch())); +} - auto error = QString(QByteArray::fromBase64(error_b64.toUtf8())).trimmed(); - auto output = QString(QByteArray::fromBase64(output_b64.toUtf8())).trimmed(); +QString NetworkController::getConnectionAddress() { + return m_connectionAddress; +} - ApplicationLogger::instance->addEntry(output); - ApplicationLogger::instance->addEntry(error); - } +void NetworkController::setConnectionAddress(QString address) { + m_connectionAddress = address; +} - if ((object.contains("type") && object["type"].toString() == "telemetry")) { - m_telimetry = Ide::IO::FromJson::telemetry(message); - emit telimetryUpdated(); - } +void NetworkController::saveSettings() { + QSettings settings("settings.ini", QSettings::IniFormat); + + settings.setValue("Network/connection_address", m_connectionAddress); + settings.setValue("Network/ping_time", m_pingTime); + settings.setValue("Network/pong_time", m_pongTime); + settings.setValue("Network/reconnect_time", m_reconnectTime); +} + +int NetworkController::getPingTime() { + return m_pingTime; +} + +int NetworkController::getPongTime() { + return m_pongTime; +} + +int NetworkController::getReconnectTime() { + return m_reconnectTime; +} + +void NetworkController::setPingTime(int value) { + m_pingTime = value; + emit timingChanged(); +} + +void NetworkController::setPongTime(int value) { + m_pongTime = value; + emit timingChanged(); +} - if ((object.contains("type") && object["type"].toString() == "request_api_token")) { - ApplicationLogger::instance->addEntry("API token required"); - m_ApiTokenDialog->open(); +void NetworkController::setReconnectTime(int value) { + m_reconnectTime = value; + emit timingChanged(); +} + +void NetworkController::onTelemetryReceived(const QString &message) +{ + auto doc = QJsonDocument::fromJson(message.toUtf8()); + auto object = doc.object(); + + if (object.contains("type")) { + QString type = object["type"].toString(); + + if (type == "output") { + auto [output_b64, error_b64] = Ide::IO::FromJson::output(message); + + auto error = QString(QByteArray::fromBase64(error_b64.toUtf8())).trimmed(); + auto output = QString(QByteArray::fromBase64(output_b64.toUtf8())).trimmed(); + + if (object.contains("shell") && object["shell"].toBool() == true) { + if (output.contains("mur-midauv-meta Version:")) { + QRegularExpression version_regexp("mur-midauv-meta Version: (\\d+)\\.(\\d+)-(\\d+)"); + auto match = version_regexp.match(output); + QVector version; + + if (match.hasMatch()) { + version << match.captured(1).toInt() + << match.captured(2).toInt() + << match.captured(3).toInt(); + + } + qDebug() << version[0] << version[1] << version[2]; + emit firmwareVersionReceived(version[0], version[1], version[2]); + } + + emit shellOutputReceived(output, error); + } else { + output.replace("\n", "
"); + error.replace("\n", "
"); + ApplicationLogger::instance->addOutput(output, error); + } + } + + if (type == "telemetry") { + m_telemetry = Ide::IO::FromJson::telemetry(message); + qDebug() << "volts: " << doc["voltage"].toDouble() + << "\tamps: " << doc["amperage"].toDouble(); + emit telemetryUpdated(); + } + + if (type == "request_api_token") { + ApplicationLogger::instance->addEntry("API token required"); + m_ApiTokenDialog->open(); + } + + if (type == "notification") { + Ide::IO::Notification notification = Ide::IO::FromJson::notification(message); + emit notificationReceived(notification.status, notification.message); + } + + if (type == "diagnostic_log_response") { + QString log = Ide::IO::FromJson::diagnostic_log(message); + emit diagnosticLogReceived(log); + } + + if (type == "software_features") { + QStringList features = Ide::IO::FromJson::software_features(message); + m_softwareFeatures.clear(); + m_softwareFeatures << features; + qDebug() << m_softwareFeatures; + + if (isFeatureSupported("set_time")) { + syncVehicleTime(); + } + } } } void NetworkController::onPongReceived(quint64, const QByteArray &) { - m_pongTimer->start(); + m_pingTimer->start(); } void NetworkController::onPingTimeout() { - m_webSocket->ping(); m_pongTimer->start(); + m_pingTimer->stop(); + m_webSocket->ping(); } void NetworkController::onPongTimeout() @@ -109,7 +217,11 @@ void NetworkController::onTokenAccepted() double NetworkController::getBatteryStatus() { - return m_telimetry.battery; + return m_telemetry.battery; +} + +bool NetworkController::isCharging() { + return m_telemetry.is_charging; } bool NetworkController::getConnectionStatus() @@ -117,69 +229,187 @@ bool NetworkController::getConnectionStatus() return m_webSocket->isValid(); } -bool NetworkController::isRomoteScriptRunning() +bool NetworkController::isRemoteScriptRunning() { - return m_telimetry.is_running; + return m_telemetry.is_running; } bool NetworkController::isRemoteModeEnabled() { - return m_telimetry.is_remote; + return m_telemetry.is_remote; } double NetworkController::getYaw() { - return m_telimetry.yaw; + return m_telemetry.yaw; } double NetworkController::getPitch() { - return m_telimetry.pitch; + return m_telemetry.roll; } double NetworkController::getRoll() { - return m_telimetry.roll; + return m_telemetry.pitch; } double NetworkController::getDepth() { - return m_telimetry.depth; + return m_telemetry.depth; } double NetworkController::getPressure() { - return m_telimetry.pressure; + return m_telemetry.pressure; +} + +double NetworkController::getTemperature() +{ + return m_telemetry.temperature; } bool NetworkController::isUsv() { - return m_telimetry.is_usv; + return m_telemetry.vehicle_type == "usv"; +} + +bool NetworkController::isRov() +{ + return m_telemetry.vehicle_type == "rov"; } double NetworkController::getLatitude() { - return m_telimetry.latitude; + return m_telemetry.latitude; } double NetworkController::getLongitude() { - return m_telimetry.longitude; + return m_telemetry.longitude; } double NetworkController::getSatellites() { - return m_telimetry.satellites; + return m_telemetry.satellites; } double NetworkController::getAltitude() { - return m_telimetry.altitude; + return m_telemetry.altitude; } double NetworkController::getSpeed() { - return m_telimetry.speed; + return m_telemetry.speed; +} + +int NetworkController::getAdcCycleCount() +{ + return m_telemetry.fg_cycle_count; +} + +int NetworkController::getAdcFullChargeCapacity() +{ + return m_telemetry.fg_full_charge_capacity; +} + +int NetworkController::getAdcMaxError() +{ + return m_telemetry.fg_max_error; +} + +int NetworkController::getAdcRemainCapacity() +{ + return m_telemetry.fg_remaining_capacity; +} + +int NetworkController::getAdcResetCount() +{ + return m_telemetry.fg_reset_count; +} + +int NetworkController::getAdcUpdateStatus() +{ + return m_telemetry.fg_update_status; +} + +double NetworkController::getAdcTemp() +{ + return m_telemetry.fg_temp; +} + +double NetworkController::getAmperage() +{ + return m_telemetry.amperage; +} + +double NetworkController::getVoltage() +{ + return m_telemetry.voltage; +} + +bool NetworkController::getAdcFlagFc() +{ + return m_telemetry.fg_flag_fc; +} + +bool NetworkController::getAdcFlagQen() +{ + return m_telemetry.fg_flag_qen; +} + +bool NetworkController::getAdcFlagRupDis() +{ + return m_telemetry.fg_flag_rup_dis; +} + +bool NetworkController::getAdcFlagVok() +{ + return m_telemetry.fg_flag_vok_flag; +} + +bool NetworkController::getAdcFlagOcvtaken() +{ + return m_telemetry.fg_flag_ocvtaken; +} + +QString NetworkController::getVehicleType() +{ + return m_telemetry.vehicle_type; +} + +QString NetworkController::getVehicleFancyName(bool html) +{ + if (m_telemetry.vehicle_type == "usv") { + return html ? "MiddleUSV" : "MiddleUSV";} + else if (m_telemetry.vehicle_type == "rov") { + return html ? "ProROV" : "ProROV";} + else { + if (m_telemetry.vehicle_name.contains("MiddleAUV-CM4")) { + return html ? "MiddleAUV-CM4" : "MiddleAUV-CM4"; + } + return html ? "MiddleAUV-CM3" : "MiddleAUV-CM3"; + } +} + +QStringList NetworkController::getSoftwareFeatures() { + return m_softwareFeatures; +} + +bool NetworkController::isFeatureSupported(const QString &feature) { + return m_softwareFeatures.contains(feature); +} + +QList NetworkController::getFcuTelemetry() { + QList fcu_telemetry = { + m_telemetry.fcu0_voltage, + m_telemetry.fcu0_amperage, + m_telemetry.fcu1_voltage, + m_telemetry.fcu1_amperage + }; + + return fcu_telemetry; } void NetworkController::setRemoteThrust(const QString &message) @@ -217,7 +447,12 @@ void NetworkController::remote() return; } - m_webSocket->sendTextMessage(Ide::IO::ToJson::remote()); + if (isFeatureSupported("gst_custom")) { + m_webSocket->sendTextMessage(Ide::IO::ToJson::remote(Ide::Ui::RemoteController::instance->getPipelines()[0])); + } else { + m_webSocket->sendTextMessage(Ide::IO::ToJson::remote()); + } + } void NetworkController::stopRemote() @@ -229,10 +464,53 @@ void NetworkController::stopRemote() m_webSocket->sendTextMessage(Ide::IO::ToJson::stop_remote()); } +void NetworkController::diagnosticLog() +{ + if (!m_webSocket->isValid()) { + return; + } + + m_webSocket->sendTextMessage(Ide::IO::ToJson::diagnostic_log()); +} + +void NetworkController::runShellCommand(QString cmd, QStringList args) +{ + if (!m_webSocket->isValid()) { + return; + } + + m_webSocket->sendTextMessage(Ide::IO::ToJson::shell_command_run(cmd, args)); +} + +void NetworkController::requestSoftwareVersion() +{ + runShellCommand("/bin/bash", + {"-c", "echo $(ver=$(dpkg -s mur-midauv-meta | grep Version); echo mur-midauv-meta ${ver:?}); sleep 1"}); +} + +void NetworkController::murUpgrade() +{ + if (!m_webSocket->isValid()) { + return; + } + + qDebug() << Ide::IO::ToJson::mur_upgrade(); + + m_webSocket->sendTextMessage(Ide::IO::ToJson::mur_upgrade()); +} + +void NetworkController::batteryButton(QString cmd){ + if (!m_webSocket->isValid()) { + return; + } + + m_webSocket->sendTextMessage(Ide::IO::ToJson::battery_command(cmd)); +} + NetworkController *NetworkController::Create() { instance = new NetworkController(); return instance; } -} // namespace ide::ui +} diff --git a/sources/NetworkController.hxx b/sources/NetworkController.hxx index 20bdbde..530aa70 100644 --- a/sources/NetworkController.hxx +++ b/sources/NetworkController.hxx @@ -5,61 +5,140 @@ #include #include +#include #include -#include "APITokenDialog.hxx" +#include "ApiTokenDialog.hxx" namespace Ide::Ui { class NetworkController : public QObject { Q_OBJECT Q_PROPERTY(bool connected READ getConnectionStatus NOTIFY connectionStatusChanged) - Q_PROPERTY(bool running READ isRomoteScriptRunning NOTIFY telimetryUpdated) - Q_PROPERTY(bool remote READ isRemoteModeEnabled NOTIFY telimetryUpdated) - - Q_PROPERTY(double battery READ getBatteryStatus NOTIFY telimetryUpdated) - Q_PROPERTY(double yaw READ getYaw NOTIFY telimetryUpdated) - Q_PROPERTY(double pitch READ getPitch NOTIFY telimetryUpdated) - Q_PROPERTY(double roll READ getRoll NOTIFY telimetryUpdated) - Q_PROPERTY(double depth READ getDepth NOTIFY telimetryUpdated) - Q_PROPERTY(double pressure READ getPressure NOTIFY telimetryUpdated) - Q_PROPERTY(bool usv READ isUsv NOTIFY telimetryUpdated) - Q_PROPERTY(double latitude READ getLatitude NOTIFY telimetryUpdated) - Q_PROPERTY(double longitude READ getLongitude NOTIFY telimetryUpdated) //altitude - Q_PROPERTY(double satellites READ getSatellites NOTIFY telimetryUpdated) - Q_PROPERTY(double altitude READ getAltitude NOTIFY telimetryUpdated) - Q_PROPERTY(double speed READ getSpeed NOTIFY telimetryUpdated) + Q_PROPERTY(bool running READ isRemoteScriptRunning NOTIFY telemetryUpdated) + Q_PROPERTY(bool remote READ isRemoteModeEnabled NOTIFY telemetryUpdated) + + Q_PROPERTY(double battery READ getBatteryStatus NOTIFY telemetryUpdated) + Q_PROPERTY(bool is_charging READ isCharging NOTIFY telemetryUpdated) + Q_PROPERTY(double yaw READ getYaw NOTIFY telemetryUpdated) + Q_PROPERTY(double pitch READ getPitch NOTIFY telemetryUpdated) + Q_PROPERTY(double roll READ getRoll NOTIFY telemetryUpdated) + Q_PROPERTY(double depth READ getDepth NOTIFY telemetryUpdated) + Q_PROPERTY(double pressure READ getPressure NOTIFY telemetryUpdated) + Q_PROPERTY(double temperature READ getTemperature NOTIFY telemetryUpdated) + Q_PROPERTY(bool usv READ isUsv NOTIFY telemetryUpdated) + Q_PROPERTY(bool rov READ isRov NOTIFY telemetryUpdated) + Q_PROPERTY(double latitude READ getLatitude NOTIFY telemetryUpdated) + Q_PROPERTY(double longitude READ getLongitude NOTIFY telemetryUpdated) + Q_PROPERTY(double satellites READ getSatellites NOTIFY telemetryUpdated) + Q_PROPERTY(double altitude READ getAltitude NOTIFY telemetryUpdated) + Q_PROPERTY(double speed READ getSpeed NOTIFY telemetryUpdated) + + Q_PROPERTY(int pingTime READ getPingTime WRITE setPingTime NOTIFY timingChanged) + Q_PROPERTY(int pongTime READ getPongTime WRITE setPongTime NOTIFY timingChanged) + Q_PROPERTY(int reconnectTime READ getReconnectTime WRITE setReconnectTime NOTIFY timingChanged) + + Q_PROPERTY(int fg_cycle_count READ getAdcCycleCount NOTIFY telemetryUpdated) + Q_PROPERTY(int fg_full_charge_capacity READ getAdcFullChargeCapacity NOTIFY telemetryUpdated) + Q_PROPERTY(int fg_max_error READ getAdcMaxError NOTIFY telemetryUpdated) + Q_PROPERTY(int fg_remaining_capacity READ getAdcRemainCapacity NOTIFY telemetryUpdated) + Q_PROPERTY(int fg_reset_count READ getAdcResetCount NOTIFY telemetryUpdated) + Q_PROPERTY(int fg_update_status READ getAdcUpdateStatus NOTIFY telemetryUpdated) + + Q_PROPERTY(double fg_temp READ getAdcTemp NOTIFY telemetryUpdated) + Q_PROPERTY(double amperage READ getAmperage NOTIFY telemetryUpdated) + Q_PROPERTY(double voltage READ getVoltage NOTIFY telemetryUpdated) + + Q_PROPERTY(bool fg_flag_fc READ getAdcFlagFc NOTIFY telemetryUpdated) + Q_PROPERTY(bool fg_flag_qen READ getAdcFlagQen NOTIFY telemetryUpdated) + Q_PROPERTY(bool fg_flag_rup_dis READ getAdcFlagRupDis NOTIFY telemetryUpdated) + Q_PROPERTY(bool fg_flag_vok_flag READ getAdcFlagVok NOTIFY telemetryUpdated) + Q_PROPERTY(bool fg_flag_ocvtaken READ getAdcFlagOcvtaken NOTIFY telemetryUpdated) + + Q_PROPERTY(QList fcu_telemetry READ getFcuTelemetry NOTIFY telemetryUpdated) + + Q_PROPERTY(QString vehicle_type READ getVehicleType NOTIFY telemetryUpdated) + Q_PROPERTY(QString vehicle_name READ getVehicleFancyName NOTIFY telemetryUpdated) public: static NetworkController *instance; static NetworkController *Create(); + QString getVehicleType(); + QString getVehicleFancyName(bool html = true); + QStringList getSoftwareFeatures(); + bool isFeatureSupported(const QString &); double getBatteryStatus(); + bool isCharging(); bool getConnectionStatus(); - bool isRomoteScriptRunning(); + bool isRemoteScriptRunning(); bool isRemoteModeEnabled(); double getYaw(); double getPitch(); double getRoll(); double getDepth(); double getPressure(); + double getTemperature(); bool isUsv(); + bool isRov(); double getLatitude(); double getLongitude(); double getSatellites(); double getAltitude(); double getSpeed(); + int getAdcCycleCount(); + int getAdcFullChargeCapacity(); + int getAdcMaxError(); + int getAdcRemainCapacity(); + int getAdcResetCount(); + int getAdcUpdateStatus(); + double getAdcTemp(); + double getAmperage(); + double getVoltage(); + + bool getAdcFlagFc(); + bool getAdcFlagQen(); + bool getAdcFlagRupDis(); + bool getAdcFlagVok(); + bool getAdcFlagOcvtaken(); + + QList getFcuTelemetry(); void setRemoteThrust(const QString &); void run(); void stop(); void remote(); void stopRemote(); + void requestFeatures(); + void syncVehicleTime(); + +public slots: + void diagnosticLog(); + void runShellCommand(QString, QStringList); + void requestSoftwareVersion(); + void murUpgrade(); + QString getConnectionAddress(); + void saveSettings(); + void setConnectionAddress(QString); + void setPingTime(int); + void setPongTime(int); + void setReconnectTime(int); + int getPingTime(); + int getPongTime(); + int getReconnectTime(); + void batteryButton(QString cmd); signals: void connectionStatusChanged(); - void telimetryUpdated(); + void telemetryUpdated(); + void notificationReceived(QString status, QString message); + void diagnosticLogReceived(QString); + void firmwareVersionReceived(int, int, int); + + void outputReceived(QString, QString); + void shellOutputReceived(QString, QString); + void timingChanged(); private: NetworkController(); @@ -68,13 +147,20 @@ private: void onConnected(); void onDisconnected(); void onReconnectTimer(); - void onTelimetryReceived(const QString &); + void onTelemetryReceived(const QString &); void onPongReceived(quint64, const QByteArray &); void onPingTimeout(); void onPongTimeout(); void onTokenAccepted(); - Ide::IO::Telemetry m_telimetry; + QString m_connectionAddress; + QStringList m_softwareFeatures; + + int m_pingTime; + int m_pongTime; + int m_reconnectTime; + + Ide::IO::Telemetry m_telemetry; QWebSocket *m_webSocket; QTimer *m_reconnectionTimer; QTimer *m_pingTimer; @@ -82,4 +168,4 @@ private: ApiTokenDialog* m_ApiTokenDialog; }; -} // namespace ide::ui +} diff --git a/sources/QmlImageItem.cpp b/sources/QmlImageItem.cpp index c8a47b6..8281daf 100644 --- a/sources/QmlImageItem.cpp +++ b/sources/QmlImageItem.cpp @@ -24,8 +24,7 @@ void QMLImageItem::paint(QPainter *painter) { QRectF bounding_rect = boundingRect(); painter->fillRect(bounding_rect, QColor("#282C34")); - /* form v0.0.6: QImage scaled = m_image.scaledToHeight(bounding_rect.height()); */ - QImage scaled = m_image.scaled(bounding_rect.width(), bounding_rect.height(), Qt::KeepAspectRatio); + QImage scaled = m_image.scaled(bounding_rect.width(), bounding_rect.height(), Qt::KeepAspectRatio, Qt::SmoothTransformation); QPointF center = bounding_rect.center() - scaled.rect().center(); if (center.x() < 0) { @@ -38,4 +37,4 @@ void QMLImageItem::paint(QPainter *painter) painter->drawImage(center, scaled); } -} // namespace ide::ui +} diff --git a/sources/QmlImageItem.hxx b/sources/QmlImageItem.hxx index c228623..b59155a 100644 --- a/sources/QmlImageItem.hxx +++ b/sources/QmlImageItem.hxx @@ -25,4 +25,4 @@ private: QImage m_image; }; -} // namespace ide::ui +} diff --git a/sources/QmlUtils.cpp b/sources/QmlUtils.cpp index 0f9e6e2..befe8e4 100644 --- a/sources/QmlUtils.cpp +++ b/sources/QmlUtils.cpp @@ -13,4 +13,4 @@ QList> &getControllersInitializersList() { return List; } -} //ui +} diff --git a/sources/QmlUtils.hxx b/sources/QmlUtils.hxx index 4bda342..689bb34 100644 --- a/sources/QmlUtils.hxx +++ b/sources/QmlUtils.hxx @@ -5,8 +5,6 @@ namespace Ide::qml { -//TODO(Vlad): Register composite type. - QList> &getControllersInitializersList(); void InitializeControllers(); @@ -16,11 +14,11 @@ struct RegisterType RegisterType() { auto initializer = []() { - qmlRegisterType(); + qmlRegisterType("", 1, 0, ""); T::Create(); }; getControllersInitializersList().append(initializer); } }; -} // namespace ide::qml +} diff --git a/sources/RemoteController.cpp b/sources/RemoteController.cpp index 21d767f..e1996f3 100644 --- a/sources/RemoteController.cpp +++ b/sources/RemoteController.cpp @@ -1,10 +1,11 @@ #include "RemoteController.hxx" -#include "Gamepad.hxx" +#include "Joystick.hxx" #include "NetworkController.hxx" #include "TextIO.hxx" #include #include +#include #include #include #include @@ -13,8 +14,6 @@ #include #include -//! TODO: Refactor this -//! namespace Ide::Ui { RemoteController *RemoteController::instance = nullptr; qml::RegisterType RemoteController::Register; @@ -25,9 +24,34 @@ RemoteController::RemoteController() throw std::runtime_error("Instance of image_view_controller already exists"); } + QSettings settings("settings.ini", QSettings::IniFormat); + settings.beginGroup("Remote"); + + m_watermark = settings.value("watermark_text", m_default_watermark).toString(); + m_watermarkOn = settings.value("watermark_on", true).toBool(); + + m_speedLimits[static_cast(SpeedModes::Low)] = settings.value("speed_low", 50).toInt(); + m_speedLimits[static_cast(SpeedModes::Mid)] = settings.value("speed_mid", 75).toInt(); + m_speedLimits[static_cast(SpeedModes::Max)] = settings.value("speed_max", 100).toInt(); + + settings.endGroup(); + m_frontImage = QImage(":/images/video_placeholder.png"); m_bottomImage = QImage(":/images/video_placeholder.png"); + + if (!IO::directoryExists(m_imagesDir)) { + QDir directory(m_imagesDir); + directory.cdUp(); + directory.mkdir("murImages"); + } + + if (!IO::directoryExists(m_imagesDir + "video")) { + QDir directory(m_imagesDir); + directory.mkdir("video"); + } + m_imageUpdateThreadFront = std::thread(&RemoteController::updateImagesFront, this); + QThread::msleep(100); m_imageUpdateThreadBottom = std::thread(&RemoteController::updateImagesBottom, this); m_transmitThrustTimer = new QTimer{}; @@ -41,9 +65,9 @@ RemoteController *RemoteController::Create() return instance; } -Gamepad *RemoteController::getGamepad() +Joystick *RemoteController::getJoystick() { - return Gamepad::instance; + return Joystick::instance; } QImage RemoteController::getFrontImage() @@ -63,16 +87,52 @@ bool RemoteController::isReadingImages() return m_isReadingImages; } +bool RemoteController::isWatermarkOn() { + return m_watermarkOn; +} + +void RemoteController::setWatermarkOn(bool state) { + m_watermarkOn = state; + + QSettings settings("settings.ini", QSettings::IniFormat); + settings.setValue("Remote/watermark_on", state); + + emit watermarkOnStateChanged(); +} + +float RemoteController::getTargetYaw() { + return m_targetYaw; +} + bool RemoteController::isAutoYaw() { return m_isAutoYaw; } +bool RemoteController::isAutoRoll() +{ + return m_isAutoRoll; +} + +bool RemoteController::isAutoPitch() +{ + return m_isAutoPitch; +} + bool RemoteController::isAutoDepth() { return m_isAutoDepth; } +bool RemoteController::isRecordingVideo() +{ + return m_isRecordingVideo; +} + +bool RemoteController::isAutoYawAltmode() { + return m_autoYawAltmode; +} + void RemoteController::startImageCapture() { if (m_isReadingImages) { @@ -91,9 +151,6 @@ void RemoteController::stopImageCapture() return; } - auto image = QImage(":/images/video_placeholder.png"); - setImageFront(image); - setImageBottom(image); m_transmitThrustTimer->stop(); m_isReadingImages = false; @@ -112,90 +169,372 @@ void RemoteController::setAutoDepth(bool val) emit autoModeChanged(); } -void RemoteController::saveImageFront() +void RemoteController::setAutoRoll(bool val) { - saveImage(m_frontImage); + m_isAutoRoll = val; + emit autoModeChanged(); } -void RemoteController::saveImageBottom() +void RemoteController::setAutoPitch(bool val) +{ + m_isAutoPitch = val; + emit autoModeChanged(); +} + +void RemoteController::setAutoYawAltmode(bool val) { + m_autoYawAltmode = val; + m_targetYaw = static_cast(NetworkController::instance->getYaw()); + emit autoModeChanged(); + emit targetYawChanged(); +} + +QString RemoteController::getTimestamp() { - saveImage(m_bottomImage); + return QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-mm-ss-zzz"); } -void RemoteController::setImageFront(QImage &front) +void RemoteController::setRecordingVideo(bool val) { - { - std::unique_lock lock(m_imageMutex); - m_frontImage = front; + m_isRecordingVideo = val; + + if (m_recordFront.isOpened()) { + m_recordFront.release(); } - emit imageChanged(); + + if (m_recordBottom.isOpened()) { + m_recordBottom.release(); + } + + if (m_isRecordingVideo) { + QString path_prefix = m_imagesDir + "video/" + getTimestamp(); + + std::string pathFront = QString(m_pipeline_record).replace("%path%", path_prefix + "_front").toStdString(); + std::string pathBottom = QString(m_pipeline_record).replace("%path%", path_prefix + "_bottom").toStdString(); + + cv::Size size_front(m_captureFront.get(cv::CAP_PROP_FRAME_WIDTH), + m_captureFront.get(cv::CAP_PROP_FRAME_HEIGHT)); + + cv::Size size_bottom(m_captureBottom.get(cv::CAP_PROP_FRAME_WIDTH), + m_captureBottom.get(cv::CAP_PROP_FRAME_HEIGHT)); + + if (size_front.width <= 0) size_front.width = 640; + if (size_front.height <= 0) size_front.height = 480; + + if (size_bottom.width <= 0) size_bottom.width = 640; + if (size_bottom.height <= 0) size_bottom.height = 480; + + m_recordFront.open(pathFront, 0, 30, size_front, true); + m_recordBottom.open(pathBottom, 0, 30, size_bottom, true); + } + + emit recordingVideoChanged(); +} + +void RemoteController::saveImageFront() +{ + saveImage(m_frontImage, "_front"); +} + +void RemoteController::saveImageBottom() +{ + saveImage(m_bottomImage, "_bottom"); } -void RemoteController::setImageBottom(QImage &bottom) +void RemoteController::setImage(QImage &destination, QImage target) { { std::unique_lock lock(m_imageMutex); - m_bottomImage = bottom; + destination = target; } - emit imageChanged(); } -void RemoteController::updateImagesFront() +void RemoteController::updateImages(cv::VideoCapture *cap, cv::VideoWriter *rec, int port, QImage *targetImg) { - cv::Mat frame_front; + std::unique_lock lock(m_capMutex); - m_captureFront.open("udpsrc port=5000 ! " - "application/x-rtp,media=video,payload=26,clock-rate=90000,encoding-name=JPEG,framerate=30/" - "1 ! rtpjpegdepay ! jpegdec ! videoconvert ! appsink drop=true sync=false"); + cv::Mat frame; + QString pipe = m_pipeline_receive; + pipe.replace("%port%", QString::number(port)); + cap->open(pipe.toStdString(), cv::CAP_GSTREAMER); + + qDebug() << "opened video on port " << port; + + lock.unlock(); + lock.release(); while (m_threadFlag) { - if (m_captureFront.read(frame_front)) { - if (!frame_front.empty()) { - QImage img = QImage((uchar *) frame_front.data, - frame_front.cols, - frame_front.rows, - QImage::Format_RGB888) - .rgbSwapped(); - setImageFront(img); + if (cap->read(frame)) { + if (!frame.empty()) { + if (m_watermarkOn) { + QString textFormat = m_watermark; + textFormat.replace("%device%", NetworkController::instance->getVehicleFancyName(false)); + textFormat.replace("%depth%", QString::number(NetworkController::instance->getDepth(), 2, 2)); + textFormat.replace("%temp%", QString::number(NetworkController::instance->getTemperature(), 2, 2)); + textFormat.replace("%yaw%", QString::number(NetworkController::instance->getYaw(), 2, 2)); + textFormat.replace("%roll%", QString::number(NetworkController::instance->getRoll(), 2, 2)); + textFormat.replace("%pitch%", QString::number(NetworkController::instance->getPitch(), 2, 2)); + textFormat.replace("%date%", QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")); + std::string text = textFormat.toStdString(); + + static cv::Point watermarkPos(4, 14); + static double watermarkScale = 0.35; + + cv::putText(frame, text, watermarkPos, cv::FONT_HERSHEY_SIMPLEX, watermarkScale, cv::Scalar::all( 50), 3, cv::LINE_AA); + cv::putText(frame, text, watermarkPos, cv::FONT_HERSHEY_SIMPLEX, watermarkScale, cv::Scalar::all(255), 1, cv::LINE_AA); + } + + QImage img = QImage((uchar *)frame.data,frame.cols, frame.rows, QImage::Format_RGB888).rgbSwapped(); + setImage(*targetImg, img); + + if (m_isRecordingVideo) { + rec->write(frame); + } } } } - m_captureFront.release(); + cap->release(); } -void RemoteController::updateImagesBottom() -{ - cv::Mat frame_bottom; +void RemoteController::updateImagesFront() { + updateImages(&m_captureFront, &m_recordFront, 5000, &m_frontImage); +} + +void RemoteController::updateImagesBottom() { + updateImages(&m_captureBottom, &m_recordBottom, 5001, &m_bottomImage); +} - m_captureBottom.open("udpsrc port=5001 ! " - "application/x-rtp,media=video,payload=26,clock-rate=90000,encoding-name=JPEG,framerate=30/" - "1 ! rtpjpegdepay ! jpegdec ! videoconvert ! appsink drop=true sync=false"); +void RemoteController::setServoPower(int device, int index, int val) { + int *currentValue; - while (m_threadFlag) { - if (m_captureBottom.read(frame_bottom)) { - if (!frame_bottom.empty()) { - QImage img = QImage((uchar *) frame_bottom.data, frame_bottom.cols, frame_bottom.rows, QImage::Format_RGB888).rgbSwapped(); - setImageBottom(img); - } + if (device == 0) { + currentValue = &m_rovControls.servos_fcu0[index]; + } else if (device == 1) { + currentValue = &m_rovControls.servos_fcu1[index]; + } else if (device == 2) { + currentValue = &m_rovControls.servos_onboard[index]; + } else { + return; + } + + if (val != *currentValue) { + emit servoValueChanged(device, index, val); + *currentValue = std::clamp(val, -100, 100); + } +} + +void RemoteController::setServoPower(int device, int index, int speed, bool button1) { + static qint64 lastTimetamp = 0; + qint64 curTimestamp = QDateTime::currentDateTime().currentMSecsSinceEpoch(); + + if (curTimestamp - lastTimetamp < 100) { + return; + } + + if (!button1) { + return; + } + + lastTimetamp = curTimestamp; + + int *currentValue; + + if (device == 0) { + currentValue = &m_rovControls.servos_fcu0[index]; + } else if (device == 1) { + currentValue = &m_rovControls.servos_fcu1[index]; + } else if (device == 2) { + currentValue = &m_rovControls.servos_onboard[index]; + } else { + return; + } + + int val = *currentValue; + if (button1) { + val += speed; + if (val > 100) { + val = -100; } } - m_captureBottom.release(); + if (val != *currentValue) { + emit servoValueChanged(device, index, val); + *currentValue = std::clamp(val, -100, 100); + } } -void RemoteController::updateRemoteThrust() -{ - if (!Gamepad::instance->getGamepad()->isConnected()) { +void RemoteController::setServoPower(int device, int index, int speed, bool button1, bool button2) { + if (button1 && button2) { + return; + } + + if (!button1 && !button2) { + return; + } + + int *currentValue; + + if (device == 0) { + currentValue = &m_rovControls.servos_fcu0[index]; + } else if (device == 1) { + currentValue = &m_rovControls.servos_fcu1[index]; + } else if (device == 2) { + currentValue = &m_rovControls.servos_onboard[index]; + } else { + return; + } + + int val = *currentValue; + if (button1) { + val -= speed; + } + + if (button2) { + val += speed; + } + + if (val != *currentValue) { + emit servoValueChanged(device, index, val); + *currentValue = std::clamp(val, -100, 100); + } +} + +void RemoteController::setServoPower(int device, int index, int valMinus, int valNeutral, int valPlus, bool button1, bool button2) { + int *currentValue; + + if (device == 0) { + currentValue = &m_rovControls.servos_fcu0[index]; + } else if (device == 1) { + currentValue = &m_rovControls.servos_fcu1[index]; + } else if (device == 2) { + currentValue = &m_rovControls.servos_onboard[index]; + } else { return; } - auto x = Gamepad::instance->getAxisXvalue(); - auto y = Gamepad::instance->getAxisYvalue(); - auto z = Gamepad::instance->getAxisZvalue(); + int val = *currentValue; + + if (button1) { + val = valMinus; + } + + if (button2) { + val = valPlus; + } + + if (button1 == button2) { + val = valNeutral; + } + + if (val != *currentValue) { + emit servoValueChanged(device, index, val); + *currentValue = std::clamp(val, -100, 100); + } +} + + +void RemoteController::setServoValue(int device, int servo, int value) { + if (device == 0) { + m_rovControls.servos_fcu0[servo] = value; + } else if (device == 1) { + m_rovControls.servos_fcu1[servo] = value; + }else if (device == 2) { + m_rovControls.servos_onboard[servo] = value; + } +} + +void RemoteController::sendRovControl(int device, int motor, int power) { + + QJsonObject json; + json["type"] = "rov_control"; + + QJsonArray main = {0, 0, 0, 0, 0, 0, 0, 0}; + QJsonArray fcu_0 = {0, 0, 0, 0, 0, 0, 0, 0}; + QJsonArray fcu_1 = {0, 0, 0, 0, 0, 0, 0, 0}; + + if (device == 0) { + main[motor] = power; + } + + if (device == 1) { + fcu_0[motor] = power; + } + + if (device == 2) { + fcu_1[motor] = power; + } + + QJsonArray fcu = {fcu_0, fcu_1}; + + json["main"] = main; + json["fcu"] = fcu; + + QJsonDocument doc(json); + QString str(doc.toJson(QJsonDocument::Compact)); + NetworkController::instance->setRemoteThrust(str); +} + + +QStringList RemoteController::getPipelines() { + QStringList pipelines; + pipelines << m_pipeline_stream << m_pipeline_receive << m_pipeline_record << m_watermark; + + return pipelines; +} + +void RemoteController::setPipelines(QString watermark) { + QSettings settings("settings.ini", QSettings::IniFormat); + + m_watermark = watermark; + + settings.beginGroup("Remote"); + settings.setValue("gst_pipeline_stream", m_pipeline_stream); + settings.setValue("gst_pipeline_receive", m_pipeline_receive); + settings.setValue("gst_pipeline_record", m_pipeline_record); + settings.setValue("watermark_text", watermark); + settings.endGroup(); +} + +int RemoteController::getSpeedMode() { + int speed = static_cast(SpeedModes::Mid); + + if (Joystick::instance->getButtonValue(JoystickAxes::SpeedSlow)) speed = static_cast(SpeedModes::Low); + if (Joystick::instance->getButtonValue(JoystickAxes::SpeedFast)) speed = static_cast(SpeedModes::Max); + + return static_cast(speed); +} + + +void RemoteController::saveSpeedLimits() { + QSettings settings("settings.ini", QSettings::IniFormat); + + settings.beginGroup("Remote"); + settings.setValue("speed_low", m_speedLimits[static_cast(SpeedModes::Low)]); + settings.setValue("speed_mid", m_speedLimits[static_cast(SpeedModes::Mid)]); + settings.setValue("speed_max", m_speedLimits[static_cast(SpeedModes::Max)]); + settings.endGroup(); +} + + +int RemoteController::axis_treshold(int value) { + float speed = m_speedLimits[getSpeedMode()] / 100.0f; + + value *= speed; - if (!isAutoYaw() && !isAutoDepth()) { + return value; +} + +void RemoteController::updateRemoteThrust() +{ + auto gamepad = Joystick::instance; + + auto x = axis_treshold(gamepad->getAxisValue(JoystickAxes::AxisY)); + auto y = axis_treshold(gamepad->getAxisValue(JoystickAxes::AxisX)); + auto z = axis_treshold(gamepad->getAxisValue(JoystickAxes::AxisZ)); + auto w = axis_treshold(gamepad->getAxisValue(JoystickAxes::AxisW)); + + if (!isAutoYaw() && !isAutoDepth() && !NetworkController::instance->isRov()) { auto first_forward_motor = static_cast(std::clamp(x + y, -100, 100)); auto second_forward_motor = static_cast(std::clamp(x - y, -100, 100)); auto first_upward_motor = static_cast(std::clamp(z, -100, 100)); @@ -222,32 +561,54 @@ void RemoteController::updateRemoteThrust() } else { QJsonObject json; - QJsonArray array = {x, y, z, int()}; + QJsonArray array = {x, y, z, w}; json["type"] = "remote_control"; + + if (isAutoYawAltmode() && NetworkController::instance->isFeatureSupported("yaw_altmode")) { + if (x != 0) { + m_targetYaw += x * 0.1; + if (m_targetYaw > 180) m_targetYaw -= 360; + if (m_targetYaw < -180) m_targetYaw += 360; + emit targetYawChanged(); + } + json["yaw_altmode"] = true; + array[0] = std::round(m_targetYaw); + } + json["axes"] = array; json["yaw"] = m_isAutoYaw; json["depth"] = m_isAutoDepth; + if (NetworkController::instance->isRov() || true) { + json["roll"] = m_isAutoRoll; + json["pitch"] = m_isAutoPitch; + + + } + QJsonDocument doc(json); QString str(doc.toJson(QJsonDocument::Compact)); NetworkController::instance->setRemoteThrust(str); } + + emit speedModeChanged(); } -void RemoteController::saveImage(const QImage &image) +void RemoteController::saveImage(const QImage &image, const QString name) { - auto dir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + "/" - + "murImages"; - if (!IO::directoryExists(dir)) { - QDir direcotory(dir); - direcotory.cdUp(); - direcotory.mkdir("murImages"); - } + auto file_name = getTimestamp() + name + ".png"; + qDebug() << m_imagesDir + file_name; + image.save(m_imagesDir + file_name); +} + +QList RemoteController::getSpeedLimits() { + return m_speedLimits; +} - auto file_name = QDateTime::currentDateTime().toString("yyyymmddhhmmsszz") + ".png"; - qDebug() << dir + "/" + file_name; - image.save(dir + "/" + file_name); +void RemoteController::setSpeedLimit(int mode, int value) { + m_speedLimits[mode] = value; + emit speedModeChanged(); } -} // namespace ide::ui +} diff --git a/sources/RemoteController.hxx b/sources/RemoteController.hxx index 18912aa..73149e9 100644 --- a/sources/RemoteController.hxx +++ b/sources/RemoteController.hxx @@ -13,7 +13,13 @@ namespace Ide::Ui { -class Gamepad; +struct RovControls { + int servos_fcu0[4] = {0}; + int servos_fcu1[4] = {0}; + int servos_onboard[6] = {0}; +}; + +class Joystick; class RemoteController : public QObject { @@ -21,45 +27,85 @@ class RemoteController : public QObject Q_PROPERTY(QImage front READ getFrontImage NOTIFY imageChanged) Q_PROPERTY(QImage bottom READ getBottomImage NOTIFY imageChanged) Q_PROPERTY(bool remote READ isReadingImages NOTIFY readingStateChanged) + Q_PROPERTY(bool watermarkOn READ isWatermarkOn WRITE setWatermarkOn NOTIFY watermarkOnStateChanged) Q_PROPERTY(bool autoDepth READ isAutoDepth WRITE setAutoDepth NOTIFY autoModeChanged) Q_PROPERTY(bool autoYaw READ isAutoYaw WRITE setAutoYaw NOTIFY autoModeChanged) + Q_PROPERTY(bool autoRoll READ isAutoRoll WRITE setAutoRoll NOTIFY autoModeChanged) + Q_PROPERTY(bool autoPitch READ isAutoPitch WRITE setAutoPitch NOTIFY autoModeChanged) + Q_PROPERTY(bool autoYawAltmode READ isAutoYawAltmode WRITE setAutoYawAltmode NOTIFY autoModeChanged) + Q_PROPERTY(float targetYaw READ getTargetYaw NOTIFY targetYawChanged) + + Q_PROPERTY(int speedMode READ getSpeedMode NOTIFY speedModeChanged) + Q_PROPERTY(QList speedLimits READ getSpeedLimits NOTIFY speedModeChanged) - Q_PROPERTY(Ide::Ui::Gamepad *Gamepad READ getGamepad CONSTANT) + Q_PROPERTY(bool recordingVideo READ isRecordingVideo WRITE setRecordingVideo NOTIFY recordingVideoChanged) + Q_PROPERTY(Ide::Ui::Joystick *Joystick READ getJoystick CONSTANT) public: static RemoteController *instance; static RemoteController *Create(); - - Gamepad *getGamepad(); + Joystick *getJoystick(); QImage getFrontImage(); QImage getBottomImage(); bool isReadingImages(); bool isAutoYaw(); + bool isAutoYawAltmode(); bool isAutoDepth(); + bool isAutoRoll(); + bool isAutoPitch(); + bool isRecordingVideo(); + bool isWatermarkOn(); + float getTargetYaw(); + int getSpeedMode(); + void setWatermarkOn(bool); void startImageCapture(); void stopImageCapture(); void setAutoYaw(bool); + void setAutoYawAltmode(bool); void setAutoDepth(bool); + void setAutoRoll(bool); + void setAutoPitch(bool); + void setRecordingVideo(bool); + + enum class SpeedModes {Low, Mid, Max}; + signals: void imageChanged(); void readingStateChanged(); void autoModeChanged(); + void targetYawChanged(); + void speedModeChanged(); + void recordingVideoChanged(); + void watermarkOnStateChanged(); + void servoValueChanged(int device, int servo, int value); public slots: void saveImageFront(); void saveImageBottom(); + void setServoValue(int device, int servo, int value); + void sendRovControl(int device, int motor, int power = 50); + void setPipelines(QString watermark); + QStringList getPipelines(); + + void setSpeedLimit(int, int); + void saveSpeedLimits(); + QList getSpeedLimits(); private: - void setImageFront(QImage &); - void setImageBottom(QImage &); + void setImage(QImage &destination, QImage target); + void updateImages(cv::VideoCapture *cap, cv::VideoWriter *rec, int port, QImage *targetImg); void updateImagesFront(); void updateImagesBottom(); void updateRemoteThrust(); + void setServoPower(int device, int index, int val); + void setServoPower(int device, int index, int speed, bool button1); + void setServoPower(int device, int index, int speed, bool button1, bool button2); + void setServoPower(int device, int index, int valMinus, int valNeutral, int valPlus, bool button1, bool button2); - void saveImage(const QImage &); + void saveImage(const QImage &, const QString name = ""); RemoteController(); static qml::RegisterType Register; @@ -71,18 +117,51 @@ private: cv::VideoCapture m_captureFront; cv::VideoCapture m_captureBottom; - cv::VideoCapture m_recordFront; - cv::VideoCapture m_recordBottom; + cv::VideoWriter m_recordFront; + cv::VideoWriter m_recordBottom; std::shared_mutex m_imageMutex; + std::mutex m_capMutex; + std::thread m_imageUpdateThreadFront; std::thread m_imageUpdateThreadBottom; std::atomic m_threadFlag = true; + + bool m_watermarkOn; + bool m_isReadingImages = false; + std::atomic m_isRecordingVideo = false; bool m_isAutoYaw = false; + bool m_isAutoRoll = false; + bool m_isAutoPitch = false; bool m_isAutoDepth = false; + + float m_targetYaw = 0.0; + bool m_autoYawAltmode = false; + + QList m_speedLimits = {50, 75, 100}; + + RovControls m_rovControls; + int axis_treshold(int value); + + QString getTimestamp(); + QString m_imagesDir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + "/murImages/"; + + QString m_watermark; + + QString m_pipeline_stream = "/usr/bin/gst-launch-1.0 -v v4l2src device=/dev/video%idx% ! videorate " + "! video/x-raw, format=YUY2, width=640, height=480, framerate=15/1 ! queue ! jpegenc quality=35 " + "! rtpjpegpay ! udpsink async=true send-duplicates=false sync=false host=%host% port=%port%"; + + QString m_pipeline_receive = "udpsrc port=%port% ! application/x-rtp,media=video,payload=26,clock-rate=90000,encoding-name=JPEG,framerate=30/1 " + "! rtpjpegdepay ! jpegdec ! videoconvert ! appsink drop=true sync=false"; + + + QString m_pipeline_record = "appsrc ! videoconvert ! video/x-raw,format=YUY2 ! queue ! jpegenc ! matroskamux ! filesink location=%path%.mkv sync=false"; + + QString m_default_watermark = "%device% / %date% / Depth: %depth% M / Temp: %temp% C / Yaw: %yaw%"; }; -} // namespace ide::ui +} diff --git a/sources/SimulatorController.cpp b/sources/SimulatorController.cpp index 1017ea8..b95fe83 100644 --- a/sources/SimulatorController.cpp +++ b/sources/SimulatorController.cpp @@ -25,7 +25,7 @@ void SimulatorController::setup_process() &SimulatorController::runningStateChanged); connect(m_simulator_process, - qOverload(&QProcess::finished), + qOverload(&QProcess::finished), this, &SimulatorController::runningStateChanged); } @@ -64,4 +64,4 @@ SimulatorController::~SimulatorController() } } -} // namespace ide::ui +} diff --git a/sources/SimulatorController.hxx b/sources/SimulatorController.hxx index f5b08be..57f2b19 100644 --- a/sources/SimulatorController.hxx +++ b/sources/SimulatorController.hxx @@ -33,4 +33,4 @@ private: QProcess *m_simulator_process = nullptr; }; -} // namespace ide::ui +} diff --git a/sources/TextIO.cpp b/sources/TextIO.cpp index c4d3838..bb5ec47 100644 --- a/sources/TextIO.cpp +++ b/sources/TextIO.cpp @@ -18,7 +18,7 @@ const QString selectSavFileUrl(const QString &title) const QString selectOpenFileUrl(const QString &filter, const QString &title) { return QFileDialog::getOpenFileName(nullptr, title, nullptr, filter); } -} // namespace from_dialog +} namespace Read { @@ -45,7 +45,7 @@ QString textFromUrl(const QString &url) return text; } -} // namespace read +} namespace Write { @@ -55,7 +55,7 @@ bool textToFile(const QString &text, const QString &url) if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { QTextStream stream(&file); - stream.setCodec("UTF-8"); + stream.setEncoding(QStringConverter::Utf8); stream.setGenerateByteOrderMark(false); stream << text.toUtf8(); file.close(); @@ -63,7 +63,7 @@ bool textToFile(const QString &text, const QString &url) } return false; } -} // namespace write +} QStringList fileNamesFromDir(const QString &dir, QStringList filters, FileSuffix option) { @@ -131,4 +131,4 @@ bool directoryExists(const QString &url) return dir.exists() && dir.isDir(); } -} // namespace ide::io +} diff --git a/sources/TextIO.hxx b/sources/TextIO.hxx index 9d90a6e..c84d2df 100644 --- a/sources/TextIO.hxx +++ b/sources/TextIO.hxx @@ -12,12 +12,12 @@ enum class FileSuffix { On, Off }; namespace FromDialog { const QString selectSavFileUrl(const QString &title = "Save File"); const QString selectOpenFileUrl(const QString &filter, const QString &title = "Open File"); -} // namespace FromDialog +} namespace Read { QJsonObject jsonFromUrl(const QString &); QString textFromUrl(const QString &); -} // namespace Read +} namespace Write { bool textToFile(const QString &text, const QString &url); @@ -28,4 +28,4 @@ QStringList fileNamesFromDir(const QString &dir, QStringList filters, FileSuffix QString fileNameFromUrl(const QString &, FileSuffix); bool fileExists(const QString &); bool directoryExists(const QString &); -} // namespace Ide::IO +} diff --git a/sources/UpdateController.cpp b/sources/UpdateController.cpp index 34690c2..b4add4e 100644 --- a/sources/UpdateController.cpp +++ b/sources/UpdateController.cpp @@ -2,7 +2,6 @@ #include #include -#include #include namespace Ide::Ui { @@ -62,14 +61,17 @@ void UpdateController::setCheckForUpdate(bool flag) void UpdateController::onCheckForUpdates() { - QNetworkConfigurationManager manager; + QTcpSocket сonnectionSocket; + сonnectionSocket.connectToHost("google.com", 80); + сonnectionSocket.waitForConnected(4000); - if (!manager.isOnline()) { + if (!(сonnectionSocket.state() == QTcpSocket::ConnectedState)) { return; } - + сonnectionSocket.close(); QProcess process; - process.start("maintenancetool --checkupdates"); + QStringList args("--checkupdates"); + process.start("maintenancetool", args); process.waitForFinished(); @@ -84,17 +86,25 @@ void UpdateController::onCheckForUpdates() return; } + if (data.contains("no updates available")) { + m_isUpdateAvailable = false; + return; + } + m_isUpdateAvailable = true; emit updateAvailable(); } void UpdateController::onUpdate() { - int ret = QMessageBox::question(nullptr, - tr("murIDE"), - tr("Update process require closing the IDE.\n" - "Do you want to proceed?"), - QMessageBox::Ok | QMessageBox::Cancel); + QMessageBox msgBox; + + msgBox.setStyleSheet("background-color: #21252B; color: #6E7582;"); + msgBox.setText("Update process require closing the IDE.\nDo you want to proceed?"); + msgBox.addButton(QMessageBox::Ok); + msgBox.addButton(QMessageBox::Cancel); + int ret = msgBox.exec(); + if (ret == QMessageBox::Cancel) { return; } @@ -116,9 +126,12 @@ void UpdateController::onCheckConnection() if (process.exitCode() == 0) { m_isConnected = true; - onCheckForUpdates(); + onCheckForUpdates(); + process.close(); return; } + + process.close(); } -} // namespace Ide::Ui +} diff --git a/sources/UpdateController.hxx b/sources/UpdateController.hxx index 5f5c2f9..2d086b1 100644 --- a/sources/UpdateController.hxx +++ b/sources/UpdateController.hxx @@ -45,4 +45,4 @@ private: bool m_isCheckForUpdate = false; }; -} // namespace Ide::Ui +} diff --git a/sources/main.cpp b/sources/main.cpp index d826f4e..3ae99ee 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -1,4 +1,6 @@ #include "Application.hxx" +#undef main +#include int main(int argc, char* argv[]) { return Ide::Ui::Application::execute(argc, argv);