How to use sig_changed method in fMBT

Best Python code snippet using fMBT_python

precisionslider.py

Source:precisionslider.py Github

copy

Full Screen

1""" A precision slider, designed after the Bauhaus sliders from the Darktable project2"""3from PyQt5.QtWidgets import (4 QApplication,5 QWidget,6 QHBoxLayout,7 QGridLayout,8 QDoubleSpinBox,9 QLabel,10 QGraphicsOpacityEffect,11)12from PyQt5.QtGui import QPainter, QColor, QPen13from PyQt5.QtCore import Qt, pyqtSignal, QPointF, QPoint14import math15from lightparam import Param, Parametrized16from lightparam.gui.controls import pretty_name, Control17class RangeSliderWidgetWithNumbers(Control, QWidget):18 sig_changed = pyqtSignal(float, float)19 def __init__(self, parametrized, name, precision=2):20 super().__init__(parametrized, name)21 self.grid_layout = QGridLayout()22 self.grid_layout.setSpacing(0)23 self.grid_layout.setContentsMargins(0, 0, 0, 0)24 self.spin_left = QDoubleSpinBox()25 self.spin_right = QDoubleSpinBox()26 min_val, max_val = parametrized.params[name].limits27 self.left, self.right = parametrized.params[name].value28 for spin in [self.spin_right, self.spin_left]:29 spin.setRange(min_val, max_val)30 spin.setDecimals(precision)31 spin.setSingleStep(10 ** (-precision))32 self.spin_left.setValue(self.left)33 self.spin_right.setValue(self.right)34 self.spin_left.valueChanged.connect(self.update_slider_left)35 self.spin_right.valueChanged.connect(self.update_slider_right)36 self.label_name = QLabel(pretty_name(name))37 self.label_name.setAlignment(Qt.AlignCenter)38 self.grid_layout.addWidget(self.spin_left, 0, 0)39 self.grid_layout.addWidget(self.label_name, 0, 1)40 self.grid_layout.addWidget(self.spin_right, 0, 2)41 self.range_slider = RangeSliderWidget(42 min_val, max_val, left=self.left, right=self.right43 )44 self.grid_layout.addWidget(self.range_slider, 1, 0, 1, 3)45 self.setLayout(self.grid_layout)46 self.range_slider.sig_changed.connect(self.update_values)47 self.update_display()48 def update_values(self, l, r):49 self.spin_left.setValue(l)50 self.spin_right.setValue(r)51 self.sig_changed.emit(l, r)52 self.left, self.right = l, r53 self.update_param()54 def update_slider_left(self, new_val):55 self.range_slider.left = new_val56 self.range_slider.update()57 self.left = new_val58 self.sig_changed.emit(self.range_slider.left, self.range_slider.right)59 self.update_param()60 def update_slider_right(self, new_val):61 self.range_slider.right = new_val62 self.range_slider.update()63 self.right = new_val64 self.sig_changed.emit(self.range_slider.left, self.range_slider.right)65 self.update_param()66 def update_display(self):67 l, r = getattr(self.parametrized, self.param_name)68 self.spin_left.setValue(l)69 self.spin_right.setValue(r)70 self.range_slider.left = l71 self.range_slider.right = r72 self.range_slider.update()73 def update_param(self):74 setattr(self.parametrized, self.param_name, (self.left, self.right))75class SliderWidgetWithNumbers(QWidget):76 sig_changed = pyqtSignal(float)77 def __init__(self, parametrized, name):78 super().__init__()79 self.parametrized = parametrized80 self.param_name = name81 self.grid_layout = QGridLayout()82 self.grid_layout.setSpacing(0)83 self.grid_layout.setContentsMargins(0, 0, 0, 0)84 self.spin_val = QDoubleSpinBox()85 self.value = parametrized.params[name].value86 min_val, max_val = parametrized.params[name].limits87 if self.value is None:88 self.value = (min_val + max_val) / 289 self.spin_val.setValue(self.value)90 self.spin_val.setRange(min_val, max_val)91 self.spin_val.setDecimals(4)92 self.spin_val.setSingleStep(0.001)93 self.spin_val.valueChanged.connect(self.update_slider)94 self.label_name = QLabel(name)95 self.label_name.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)96 self.grid_layout.addWidget(self.label_name, 0, 0)97 self.grid_layout.addWidget(self.spin_val, 0, 1)98 self.slider = PrecisionSingleSlider(min_val, max_val, default_value=self.value)99 self.grid_layout.addWidget(self.slider, 1, 0, 1, 2)100 self.setLayout(self.grid_layout)101 self.slider.sig_changed.connect(self.update_values)102 def update_values(self, val):103 self.spin_val.setValue(val)104 self.value = val105 self.sig_changed.emit(val)106 self.update_param()107 def update_slider(self, new_val):108 self.slider.pos = new_val109 self.slider.update()110 self.value = new_val111 self.sig_changed.emit(new_val)112 self.update_param()113 def update_display(self):114 val = getattr(self.parametrized, self.param_name)115 self.slider.pos = val116 self.spin_val.setValue(val)117 self.slider.update()118 def update_param(self):119 setattr(self.parametrized, self.param_name, self.value)120class SliderPopupLines(QWidget):121 """ A widget that displays the guiding lines for the fine adjustment122 """123 def __init__(self, *args, f_line, **kwargs):124 super().__init__(*args, **kwargs)125 self.f_line = f_line126 self.fadeub = QGraphicsOpacityEffect(self)127 self.current_value = 0128 def set_current_value(self, val):129 self.current_value = val130 self.update()131 def paintEvent(self, e):132 size = self.size()133 w = size.width()134 h = size.height()135 halfw = w / 2136 dy = 1137 qp = QPainter()138 qp.begin(self)139 # qp.setRenderHint(QPainter.Antialiasing)140 qp.setPen(Qt.NoPen)141 qp.setBrush(QColor(0, 0, 0, 70))142 qp.drawRoundedRect(0, 0, w, h, 3, 3)143 qp.setPen(QColor(100, 100, 100))144 for xs in [-halfw * 3 / 4, -halfw / 2, -halfw / 4, -halfw / 8, -halfw / 16]:145 for coeff in [-1, 1]:146 x_s = xs * coeff147 x_p = x_s148 y_p = dy149 for y in range(dy * 2, h, dy):150 x = x_s * self.f_line(y)151 qp.drawLine(x_p + halfw, y_p, x + halfw, y)152 x_p = x153 y_p = y154 x_s = self.current_value155 x_p = x_s156 y_p = 0157 qp.setPen(QColor(250, 250, 250))158 for y in range(dy, h, dy):159 x = x_s * self.f_line(y)160 qp.drawLine(x_p + halfw, y_p, x + halfw, y)161 x_p = x162 y_p = y163 qp.end()164class PrecisionSlider(QWidget):165 def __init__(self, min=0.0, max=1.0, max_magnification=50, magnifier_height=200):166 super().__init__()167 self.min_val = min168 self.max_val = max169 # display geometry170 self.padding_top = 15171 self.padding_side = 0172 self.default_color = QColor(200, 200, 200)173 self.highlight_color = QColor(30, 100, 200)174 self.triangle_size = 8175 self.magnifier_height = magnifier_height176 self.max_magnification = max_magnification177 self.square_coef = (178 1 - 1 / self.max_magnification179 ) / self.magnifier_height ** 1.5180 self.mouse_status = 0181 self.mouse_start_x = 0182 self.mouse_start_y = 0183 self.popup = SliderPopupLines(self, f_line=self.f_line)184 self.popup.setWindowFlags(185 Qt.Tool | Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint186 )187 self.popup.setAttribute(Qt.WA_ShowWithoutActivating)188 self.popup.setAttribute(Qt.WA_TranslucentBackground, True)189 self.setMinimumHeight(self.padding_top * 3)190 def _equilateral_triangle_points(self, origin):191 h = self.triangle_size192 w = self.triangle_size / (math.sqrt(3))193 return origin, (origin[0] - w, origin[1] + h), (origin[0] + w, origin[1] + h)194 def paintEvent(self, e):195 qp = QPainter()196 qp.begin(self)197 self.drawWidget(qp)198 qp.end()199 def f_amp(self, y):200 """ Function which maps a y position of the mouse to an amplification factor201 :param y:202 :return:203 """204 # TODO a more reasonable one205 return 1 - self.square_coef * min(y, self.magnifier_height) ** 1.5206 def f_line(self, y):207 """ Function that gives the208 :param coef:209 :param magnifier_height:210 :return:211 """212 return 1 / (1 - self.square_coef * min(y, self.magnifier_height) ** 1.5)213 def val_to_vis(self, val):214 size = self.size()215 w = size.width()216 p = self.padding_side217 return int(218 round(219 p + (w - 2 * p) * (val - self.min_val) / (self.max_val - self.min_val)220 )221 )222 def vis_to_val_relative(self, vis_rel):223 size = self.size()224 w = size.width()225 p = self.padding_side226 return (self.max_val - self.min_val) * vis_rel / (w - 2 * p)227 def vis_to_val(self, vis):228 size = self.size()229 w = size.width()230 p = self.padding_side231 return self.min_val + (self.max_val - self.min_val) * (vis - p) / (w - 2 * p)232 def mouseReleaseEvent(self, QMouseEvent):233 self.mouse_status = 0234 self.popup.hide()235 self.update()236class PrecisionSingleSlider(PrecisionSlider):237 sig_changed = pyqtSignal(float)238 def __init__(self, *args, default_value=None, **kwargs):239 super().__init__(*args, **kwargs)240 if default_value is None:241 self.pos = (self.min_val + self.max_val) / 2242 else:243 self.pos = default_value244 # GUI helpers245 self.triangle = None246 self.bar_shift = 0247 self.old_pos = 0248 def drawWidget(self, qp):249 size = self.size()250 w = size.width()251 h = size.height()252 pt = self.padding_top253 ps = self.padding_side254 qp.setPen(QColor(100, 100, 100))255 qp.drawLine(ps, pt, w - ps, pt)256 qp.setPen(Qt.NoPen)257 qp.setBrush(self.default_color)258 lv = self.val_to_vis(self.pos)259 self.triangle = self._equilateral_triangle_points((lv, pt))260 for triangle, label in zip([self.triangle], [1]):261 if self.mouse_status == label:262 qp.setBrush(self.highlight_color)263 else:264 qp.setBrush(self.default_color)265 qp.drawPolygon(*map(lambda point: QPointF(*point), triangle))266 def mousePressEvent(self, ev):267 self.old_pos = self.pos268 self.mouse_start_x = ev.x()269 self.mouse_start_y = ev.y()270 self.mouse_status = 0271 # check if mouse is pressed on any of the handles272 triangle = self.triangle273 if (triangle[1][0] < self.mouse_start_x < triangle[2][0]) and (274 triangle[0][1] < self.mouse_start_y < triangle[1][1]275 ):276 self.mouse_status = 1277 # check if mose is pressed on the bar278 if self.mouse_status > 0:279 self.mouse_start_y = triangle[1][1]280 self.popup.show()281 global_xy = self.mapToGlobal(QPoint(self.mouse_start_x, self.mouse_start_y))282 self.popup.setGeometry(283 global_xy.x() - self.magnifier_height // 2,284 global_xy.y(),285 self.magnifier_height,286 self.magnifier_height,287 )288 def set_pos_vis(self, visval):289 self.pos = min(self.max_val, max(self.min_val, self.vis_to_val(visval)))290 def mouseMoveEvent(self, ev):291 x = ev.x()292 delta = x - self.mouse_start_x293 amplification = self.f_amp(abs(ev.y() - self.mouse_start_y))294 x_n = self.vis_to_val_relative(delta * amplification)295 if self.mouse_status == 1:296 self.pos = min(max(self.old_pos + x_n, self.min_val), self.max_val)297 self.update()298 self.popup.set_current_value(delta * amplification)299 self.sig_changed.emit(self.pos)300class RangeSliderWidget(PrecisionSlider):301 sig_changed = pyqtSignal(float, float)302 def __init__(self, *args, left=None, right=None, **kwargs):303 super().__init__(*args, **kwargs)304 centre = (self.min_val + self.max_val) / 2305 range = self.max_val - self.min_val306 if right is None:307 self.right = centre + range / 10308 else:309 self.right = right310 if left is None:311 self.left = centre - range / 10312 else:313 self.left = left314 self.barwidth = 2315 # GUI helpers316 self.l_triangle = None317 self.r_triangle = None318 self.bar_shift = 0319 self.old_left = 0320 self.old_right = 0321 def drawWidget(self, qp):322 size = self.size()323 w = size.width()324 h = size.height()325 pt = self.padding_top326 ps = self.padding_side327 qp.setPen(QColor(100, 100, 100))328 qp.drawLine(ps, pt, w - ps, pt)329 qp.setPen(Qt.NoPen)330 qp.setBrush(self.default_color)331 lv = self.val_to_vis(self.left)332 rv = self.val_to_vis(self.right)333 if self.mouse_status == 3:334 qp.setBrush(self.highlight_color)335 else:336 qp.setBrush(self.default_color)337 qp.drawRect(lv, pt, rv - lv, self.barwidth)338 qp.setRenderHint(QPainter.Antialiasing)339 self.l_triangle, self.r_triangle = (340 self._equilateral_triangle_points((val, pt + self.barwidth))341 for val in [lv, rv]342 )343 for triangle, label in zip([self.l_triangle, self.r_triangle], [1, 2]):344 if self.mouse_status == label:345 qp.setBrush(self.highlight_color)346 else:347 qp.setBrush(self.default_color)348 qp.drawPolygon(*map(lambda point: QPointF(*point), triangle))349 def mousePressEvent(self, ev):350 self.old_left = self.left351 self.old_right = self.right352 self.mouse_start_x = ev.x()353 self.mouse_start_y = self.l_triangle[1][1]354 self.mouse_status = 0355 # check if mouse is pressed on any of the handles356 for triangle, label in zip([self.l_triangle, self.r_triangle], [1, 2]):357 if (triangle[1][0] < self.mouse_start_x < triangle[2][0]) and (358 triangle[0][1] < ev.y() < triangle[1][1]359 ):360 self.mouse_status = label361 # check if mose is pressed on the bar362 if (self.l_triangle[0][0] < self.mouse_start_x < self.r_triangle[0][0]) and (363 self.l_triangle[0][1] - self.barwidth - 2364 < ev.y()365 < self.l_triangle[0][1] + 2366 ):367 self.mouse_status = 3368 if self.mouse_status > 0:369 global_xy = self.mapToGlobal(QPoint(self.mouse_start_x, self.mouse_start_y))370 self.popup.show()371 self.popup.setGeometry(372 global_xy.x() - self.magnifier_height // 2,373 global_xy.y(),374 self.magnifier_height,375 self.magnifier_height,376 )377 def set_left_vis(self, visval):378 self.left = max(self.min_val, self.vis_to_val(visval))379 def set_right_vis(self, visval):380 self.right = max(self.max_val, self.vis_to_val(visval))381 def set_lr(self, l=None, r=None):382 if l is None:383 l = self.left384 if r is None:385 r = self.right386 if (r > l) and (l > self.min_val) and (r < self.max_val):387 self.left = l388 self.right = r389 def mouseMoveEvent(self, ev):390 x = ev.x()391 delta = x - self.mouse_start_x392 amplification = self.f_amp(abs(ev.y() - self.mouse_start_y))393 x_n = self.vis_to_val_relative(delta * amplification)394 if QApplication.instance().keyboardModifiers() == Qt.AltModifier:395 if self.mouse_status == 1:396 self.set_lr(self.old_left + x_n, self.old_right - x_n)397 elif self.mouse_status == 2:398 self.set_lr(self.old_left - x_n, self.old_right + x_n)399 if self.mouse_status == 1:400 self.set_lr(l=self.old_left + x_n)401 elif self.mouse_status == 2:402 self.set_lr(r=self.old_right + x_n)403 # if we drag the bar, move the bar404 elif self.mouse_status == 3:405 self.set_lr(self.old_left + x_n, self.old_right + x_n)406 self.update()407 self.popup.set_current_value(delta * amplification)408 self.sig_changed.emit(self.left, self.right)409if __name__ == "__main__":410 import qdarkstyle411 class TestP(Parametrized):412 def __init__(self):413 super().__init__()414 self.x = Param((1.0, 2.0), (0.0, 100.0))415 app = QApplication([])416 app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())417 win = QWidget()418 layout = QHBoxLayout()419 win.setLayout(layout)420 p = TestP()421 slider_1 = RangeSliderWidgetWithNumbers(p, "x")422 layout.addWidget(slider_1)423 win.show()...

Full Screen

Full Screen

asciimol.py

Source:asciimol.py Github

copy

Full Screen

1import curses2from math import ceil3from time import sleep4from asciimol.app.renderer import Renderer5from asciimol.app.config import Config6class AsciiMol:7 def __init__(self):8 self.stdscr = None9 self.renderer = None10 self.sig_changed = True11 self.config = Config()12 self.frames = 013 self.timeout = 014 def redraw(self):15 self.stdscr.clear()16 self.draw_characters(self.renderer.content)17 self.draw_navbar()18 self.stdscr.refresh()19 self.sig_changed = False20 def draw_characters(self, content: list):21 for i in range(curses.LINES - 2):22 for j in range(curses.COLS):23 char, col = content[i][j].split(",")24 self.stdscr.addstr(i, j, char, int(col))25 def draw_navbar(self):26 x, y, z = self.renderer.rotcounter27 ztoggle_str = "Z" if self.renderer.ztoggle else "Y"28 navbar_string = "[Q]uit [R]eset "29 navbar_string += "[B]onds %s " % ("on " if self.renderer.btoggle else "off")30 navbar_string += "[+-] Zoom (%- 3.3f) " % self.renderer.zoom31 navbar_string += "[↔↕] Rotate (%-3.f, %-3.f, %-3.f) " % (x, y, z)32 navbar_string += "[Z] ↔ Y/Z rotation (%s) " % ztoggle_str33 navbar_string += "[WSAD] Navigate "34 navbar_string += "[T] Principle Axes "35 navbar_string += "[F1-3] Auto-Rotate"36 try:37 self.stdscr.addstr(navbar_string)38 except curses.error:39 if curses.LINES > 1 and curses.COLS > 3:40 self.stdscr.addstr(curses.LINES - 1, curses.COLS - 4, "...")41 def handle_keypresses(self, keys):42 self.sig_changed = False43 if len(keys) > 0:44 if 87 in keys or 119 in keys: # W45 self.sig_changed = self.renderer.navigate(dy=-ceil(self.renderer.zoom))46 if 83 in keys or 115 in keys: # S47 self.sig_changed = self.renderer.navigate(dy=ceil(self.renderer.zoom))48 if 65 in keys or 97 in keys: # A49 self.sig_changed = self.renderer.navigate(dx=-ceil(self.renderer.zoom))50 if 68 in keys or 100 in keys: # D51 self.sig_changed = self.renderer.navigate(dx=ceil(self.renderer.zoom))52 if 84 in keys or 116 in keys: # T53 self.sig_changed = self.renderer.prinicple_axes()54 if curses.KEY_F1 in keys:55 self.renderer.toggle_auto_rotate(x=True)56 if curses.KEY_F2 in keys:57 self.renderer.toggle_auto_rotate(y=True)58 if curses.KEY_F3 in keys:59 self.renderer.toggle_auto_rotate(z=True)60 if curses.KEY_DOWN in keys:61 self.sig_changed = self.renderer.rotate(x=1)62 if curses.KEY_UP in keys:63 self.sig_changed = self.renderer.rotate(x=-1)64 if curses.KEY_LEFT in keys:65 self.sig_changed = self.renderer.rotate(z=1) if self.renderer.ztoggle else self.renderer.rotate(y=1)66 if curses.KEY_RIGHT in keys:67 self.sig_changed = self.renderer.rotate(z=-1) if self.renderer.ztoggle else self.renderer.rotate(y=-1)68 if 43 in keys: # +69 self.sig_changed = self.renderer.modify_zoom(0.1)70 if 45 in keys: # -71 self.sig_changed = self.renderer.modify_zoom(-0.1)72 if 82 in keys or 114 in keys: # R73 self.sig_changed = self.renderer.reset_view()74 if 66 in keys or 98 in keys: # B75 self.renderer.btoggle = not self.renderer.btoggle76 self.sig_changed = True77 if 90 in keys or 122 in keys: # Z78 self.renderer.ztoggle = not self.renderer.ztoggle79 self.sig_changed = True80 if 81 in keys or 113 in keys: # Q81 return False82 return True83 def on_update(self):84 keys = []85 key = self.stdscr.getch()86 while key is not curses.ERR:87 keys.append(key)88 key = self.stdscr.getch()89 running = self.handle_keypresses(keys)90 # Limit resizing to once per second, this is easier on curses.91 if self.frames == 59:92 if curses.is_term_resized(self.renderer.height, self.renderer.width):93 curses.update_lines_cols()94 self.renderer.resize(curses.LINES, curses.COLS)95 self.sig_changed = True96 self.frames = 097 # Auto-Rotation at 30 fps to reduce workload98 if self.renderer.get_auto_rotate() and self.frames % 2 == 0:99 self.renderer.auto_rotate()100 self.sig_changed = True101 if self.sig_changed:102 self.renderer.buffer_scene()103 try:104 self.redraw()105 self.timeout = 0106 except curses.error:107 # Try again next update, this could hang, so do a timeout counter108 self.timeout += 1109 if self.timeout > 100:110 raise RuntimeError("Curses had an irrecoverable problem.")111 self.frames += 1112 return running113 def main_loop(self, main_screen):114 # The internal color setup requires curses to be initialized first115 self.config.post_setup()116 # Save curses main screen for reference117 self.stdscr = main_screen118 # Init a new renderer of appropriate size119 self.renderer = Renderer(curses.LINES, curses.COLS, self.config)120 # Turns off cursor121 curses.curs_set(0)122 # Turns off hangup on input polling123 self.stdscr.nodelay(True)124 running = True125 self.redraw()126 while running:127 try:128 # Running at 60 fps129 sleep(1 / 60)130 running = self.on_update()131 except (KeyboardInterrupt, SystemError, SystemExit):132 running = False133 def run(self):134 if self.config.parse():135 curses.wrapper(self.main_loop)...

Full Screen

Full Screen

flashlight_model.py

Source:flashlight_model.py Github

copy

Full Screen

1# qflashlight - Simple Qt-based fullscreen flashlight2# Copyright (C) 2017 Ingo Ruhnke <grumbel@gmail.com>3#4# This program is free software: you can redistribute it and/or modify5# it under the terms of the GNU General Public License as published by6# the Free Software Foundation, either version 3 of the License, or7# (at your option) any later version.8#9# This program is distributed in the hope that it will be useful,10# but WITHOUT ANY WARRANTY; without even the implied warranty of11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12# GNU General Public License for more details.13#14# You should have received a copy of the GNU General Public License15# along with this program. If not, see <http://www.gnu.org/licenses/>.16from PyQt5.QtCore import Qt, QObject, pyqtSignal17from PyQt5.QtGui import QColor, QFont18class FlashlightModel(QObject):19 sig_changed = pyqtSignal()20 def __init__(self) -> None:21 super().__init__()22 self._bg_color: QColor = QColor(Qt.black)23 self._fg_color: QColor = QColor(Qt.white)24 self._font: QFont = QFont()25 self._text: str = ""26 def foreground_color(self) -> QColor:27 return self._fg_color28 def background_color(self) -> QColor:29 return self._bg_color30 def font(self) -> QFont:31 return self._font32 def text(self) -> str:33 return self._text34 def set_foreground_color(self, color: QColor) -> None:35 self._fg_color = color36 self.sig_changed.emit()37 def set_background_color(self, color: QColor) -> None:38 self._bg_color = color39 self.sig_changed.emit()40 def set_font(self, font: QFont) -> None:41 self._font = font42 self.sig_changed.emit()43 def set_text(self, text: str) -> None:44 self._text = text45 self.sig_changed.emit()...

Full Screen

Full Screen

Automation Testing Tutorials

Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.

LambdaTest Learning Hubs:

YouTube

You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.

Run fMBT automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

Try LambdaTest Now !!

Get 100 minutes of automation test minutes FREE!!

Next-Gen App & Browser Testing Cloud

Was this article helpful?

Helpful

NotHelpful