File: utils.py

package info (click to toggle)
raysession 0.17.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 19,168 kB
  • sloc: python: 44,371; sh: 1,538; makefile: 208; xml: 86
file content (385 lines) | stat: -rw-r--r-- 11,799 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# PatchBay Canvas engine using QGraphicsView/Scene
# Copyright (C) 2010-2019 Filipe Coelho <falktx@falktx.com>
# Copyright (C) 2019-2024 Mathieu Picot <picotmathieu@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of
# the License, or any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# For a full copy of the GNU General Public License see the doc/GPL.txt file.


import logging
from typing import TYPE_CHECKING

from qtpy.QtCore import QPointF, QFile, QRectF
from qtpy.QtGui import QIcon, QPalette
from qtpy.QtWidgets import QWidget

from patshared import PortMode, BoxType
from .init_values import canvas, options

if TYPE_CHECKING:
    from .box_widget_moth import BoxWidgetMoth

_logger = logging.getLogger(__name__)
_logging_str = ''

_PG_NAME_ENDS = (' ', '_', '.', '-', '#', ':', 'out', 'in', 'Out',
                 'In', 'Output', 'Input', 'output', 'input',
                 ' AUX', '_AUX')

# decorator
def easy_log(func):
    ''' decorator for API callable functions.
        It makes debug logs and also a global logging string
        usable directly in the functions'''
    def wrapper(*args, **kwargs):
        args_strs = [str(arg) for arg in args]
        args_strs += [f"{k}={v}" for k, v in kwargs.items()]

        global _logging_str
        _logging_str = f"{func.__name__}({', '.join(args_strs)})"
        _logger.debug(_logging_str)
        return func(*args, **kwargs)
    return wrapper

def get_new_group_positions() -> dict[PortMode, tuple[int, int]]:
    def get_middle_empty_positions(scene_rect: QRectF) -> tuple[int, int]:
        if scene_rect.isNull():
            return ((0, 200))

        needed_x = 120
        needed_y = 120
        margin_x = 50
        margin_y = 10

        x = scene_rect.center().x() - needed_y / 2
        y = scene_rect.top() + 20

        y_list = list[tuple[float, float, float]]()

        min_top = scene_rect.bottom()
        max_bottom = scene_rect.top()

        for widget in canvas.list_boxes():
            box_rect = widget.sceneBoundingRect()
            min_top = min(min_top, box_rect.top())
            max_bottom = max(max_bottom, box_rect.bottom())

            if box_rect.left() - needed_x <= x <= box_rect.right() + margin_x:
                y_list.append(
                    (box_rect.top(), box_rect.bottom(), box_rect.left()))

        if not y_list:
            return (int(x), int(y))

        y_list.sort()
        available_segments = [[min_top, max_bottom, x]]

        for box_top, box_bottom, box_left in y_list:
            for segment in available_segments:
                seg_top, seg_bottom, seg_left = segment

                if box_bottom <= seg_top or box_top >= seg_bottom:
                    continue

                if box_top <= seg_top and box_bottom >= seg_bottom:
                    available_segments.remove(segment)
                    break

                if box_top > seg_top:
                    segment[1] = box_top
                    if box_bottom < seg_bottom:
                        available_segments.insert(
                            available_segments.index(segment) + 1,
                            [box_bottom, seg_bottom, box_left])
                        break

                segment[0] = box_bottom

        if not available_segments:
            return (int(x), int(max_bottom + margin_y))

        available_segments.sort()

        for seg_top, seg_bottom, seg_left in available_segments:
            if seg_bottom - seg_top >= 200:
                y = seg_top + margin_y
                x = seg_left
                break
        else:
            y = max_bottom + margin_y

        return (int(x), int(y))

    canvas.ensure_init()
    rect = canvas.scene.get_new_scene_rect()
    if rect.isNull():
        return {PortMode.BOTH: (200, 0),
                PortMode.INPUT: (400, 0),
                PortMode.OUTPUT: (0, 0)}

    y = rect.bottom()

    return {PortMode.BOTH: get_middle_empty_positions(rect),
            PortMode.INPUT: (400, int(y)),
            PortMode.OUTPUT: (0, int(y))}

def get_portgroup_name_from_ports_names(ports_names: list[str]) -> str:
    if len(ports_names) < 2:
        return ''

    # set portgrp name
    portgrp_name = ''

    for c in ports_names[0]:
        for eachname in ports_names:
            if not eachname.startswith(portgrp_name + c):
                break
        else:
            portgrp_name += c
    
    # reduce portgrp name until it ends with one of the patterns
    # in portgrp_name_ends
    while portgrp_name:
        if (portgrp_name.endswith((_PG_NAME_ENDS))
                or portgrp_name in ports_names):
            break
        
        portgrp_name = portgrp_name[:-1]
    
    return portgrp_name

def portgroup_name_splitted(
        *port_names: str) -> tuple[str, tuple[str]]:
    '''return a tuple of two elements,
    the first element is the portgroup name,
    the second is another tuple containing a suffix for each port. 
    '''
    if len(port_names) <= 0:
        return ('', tuple[str]())
    if len(port_names) <= 1:
        return (port_names[0], ('',))

    # set portgrp name
    portgrp_name = ''

    for c in port_names[0]:
        for eachname in port_names:
            if not eachname.startswith(portgrp_name + c):
                break
        else:
            portgrp_name += c
    
    # reduce portgrp name until it ends with one of the patterns
    # in portgrp_name_ends
    while portgrp_name:
        if (portgrp_name.endswith((_PG_NAME_ENDS))
                or portgrp_name in port_names):
            break
        
        portgrp_name = portgrp_name[:-1]
    
    port_suffixes = list[str]()
    for port_name in port_names:
        port_suffixes.append(port_name.replace(portgrp_name, '', 1))
    
    return (portgrp_name, tuple(port_suffixes))

def get_icon(icon_type: BoxType, icon_name: str,
             port_mode: PortMode, dark=True) -> QIcon:
    if icon_type in (BoxType.CLIENT, BoxType.APPLICATION):
        icon = QIcon.fromTheme(icon_name)

        if icon.isNull():
            for ext in ('svg', 'svgz', 'png'):
                filename = ":app_icons/%s.%s" % (icon_name, ext)

                if QFile.exists(filename):
                    del icon
                    icon = QIcon()
                    icon.addFile(filename)
                    break
        return icon

    icon = QIcon()

    match icon_type:
        case BoxType.HARDWARE:
            icon_file = ":/canvas/"
            icon_file += "dark/" if dark else "light/"
            
            if icon_name == "a2j":
                icon_file += "DIN-5.svg"        
            else:
                if port_mode is PortMode.INPUT:
                    icon_file += "audio-headphones.svg"
                elif port_mode is PortMode.OUTPUT:
                    icon_file += "microphone.svg"
                else:
                    icon_file += "pb_hardware.svg"

            icon.addFile(icon_file)

        case BoxType.MONITOR:
            prefix = ":/canvas/"
            prefix += "dark/" if dark else "light/"
            
            if port_mode is PortMode.INPUT:
                icon.addFile(prefix + "monitor_capture.svg")
            else:
                icon.addFile(prefix + "monitor_playback.svg")

        case BoxType.INTERNAL:
            icon.addFile(":/scalable/%s" % icon_name)

    return icon

def is_dark_theme(widget: QWidget) -> bool:
    return bool(
        widget.palette().brush(QPalette.ColorGroup.Active,
                               QPalette.ColorRole.WindowText).color().lightness()
        > 128)

def boxes_in_dict(boxes: 'list[BoxWidgetMoth]') -> dict[int, PortMode]:
    '''concatenate a list of boxes to have a dict
    where key is group_id.'''
    serial_dict = dict[int, PortMode]()
    for box in boxes:
        pmode = serial_dict.get(box._group_id)
        if pmode is None:
            serial_dict[box._group_id] = box._port_mode
        else:
            serial_dict[box._group_id] |= box._port_mode
    return serial_dict

def nearest_on_grid(xy: tuple[int, int]) -> tuple[int, int]:
    canvas.ensure_init()
    x, y = xy
    cell_x = options.cell_width
    cell_y = options.cell_height
    margin = canvas.theme.box_spacing // 2

    ret_x = cell_x * (x // cell_x) + margin
    if x - ret_x > cell_x / 2:
        ret_x += cell_x
    
    ret_y = cell_y * (y // cell_y) + margin
    if y - ret_y > cell_y / 2:
        ret_y += cell_y
    
    return (ret_x, ret_y)

def nearest_on_grid_check_others(
        xy: tuple[int, int], orig_box: 'BoxWidgetMoth') -> tuple[int, int]:
    '''return the pos for a just moved box,
    may be not exactly the nearest point on grid,
    to prevent unwanted other boxes move.'''
    canvas.ensure_init()
    spacing = canvas.theme.box_spacing
    check_rect = orig_box.boundingRect().translated(QPointF(*xy))    
    search_rect = check_rect.adjusted(- spacing, - spacing, spacing, spacing)

    boxes = [b for b in canvas.scene.list_boxes_at(search_rect)
             if b is not orig_box]
    x, y = xy
    new_x, new_y = nearest_on_grid(xy)
    
    for box in boxes:
        rect = box.sceneBoundingRect()

        if (previous_top_on_grid(y)
                == previous_top_on_grid(rect.bottom())):
            return (new_x, previous_top_on_grid(y) + options.cell_height)
        
        if (next_bottom_on_grid(check_rect.bottom())
                == next_bottom_on_grid(rect.top())):
            return (new_x, next_top_on_grid(y) - options.cell_height)
     
    return nearest_on_grid(xy)

def previous_left_on_grid(x: int | float) -> int:
    canvas.ensure_init()
    cell_x = options.cell_width
    margin = canvas.theme.box_spacing / 2
    
    ret = int(cell_x * (x // cell_x) + margin)
    if ret > x:
        ret -= cell_x
    
    return ret

def next_left_on_grid(x: int | float) -> int:
    canvas.ensure_init()
    cell_x = options.cell_width
    margin = canvas.theme.box_spacing / 2
    
    ret = int(cell_x * (x // cell_x) + margin)
    if ret < x:
        ret += cell_x
    
    return ret

def previous_top_on_grid(y: int | float) -> int:
    canvas.ensure_init()
    cell_y = options.cell_height
    margin = canvas.theme.box_spacing / 2
    
    ret = int(cell_y * (y // cell_y) + margin)
    if ret > y:
        ret -= cell_y
    
    return ret

def next_top_on_grid(y: int | float) -> int:
    canvas.ensure_init()
    cell_y = options.cell_height
    margin = canvas.theme.box_spacing / 2
    
    ret = int(cell_y * ((y - 1) // cell_y) + margin)
    if ret < y:
        ret += cell_y

    return ret

def next_bottom_on_grid(y: int | float) -> int:
    canvas.ensure_init()
    cell_y = options.cell_height
    margin = canvas.theme.box_spacing / 2

    ret = int(cell_y * (1 + y // cell_y) - margin)
    if ret < y:
        ret += cell_y

    return ret

def next_width_on_grid(width: int | float) -> int:
    canvas.ensure_init()
    cell_x = options.cell_width
    box_spacing = canvas.theme.box_spacing
    ret = cell_x * (1 + (width // cell_x)) - box_spacing
    while ret < width:
        ret += cell_x
    
    return int(ret)

def next_height_on_grid(height: int | float) -> int:
    canvas.ensure_init()
    cell_y = options.cell_height
    box_spacing = canvas.theme.box_spacing
    ret = cell_y * (1 + (height // cell_y)) - box_spacing
    while ret < height:
        ret += cell_y
    
    return int(ret)