Skip to content

Commit c4020e0

Browse files
authored
Merge pull request #347 from tinymovr/studio/GUI_console
Add GUI console
2 parents 83b7116 + 7c82586 commit c4020e0

File tree

5 files changed

+181
-13
lines changed

5 files changed

+181
-13
lines changed

studio/Python/setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"docopt",
6161
"flatten-dict",
6262
"pint",
63+
"pretty_errors"
6364
],
6465
extras_require={"gui": ["pyside6", "pyqtgraph>=0.13.3"]},
6566
entry_points={

studio/Python/tinymovr/gui/__init__.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
display_file_open_dialog,
88
display_file_save_dialog,
99
magnitude_of,
10+
StreamRedirector,
11+
QTextBrowserLogger,
1012
TimedGetter,
1113
check_selected_items,
1214
get_dynamic_attrs,
1315
is_dark_mode,
14-
strtobool
16+
strtobool,
17+
configure_pretty_errors
1518
)
1619
from tinymovr.gui.widgets import (
1720
NodeTreeWidgetItem,

studio/Python/tinymovr/gui/helpers.py

+75-1
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,29 @@
1616
"""
1717

1818
import time
19+
import logging
20+
from datetime import datetime
1921
import os
2022
import enum
2123
import pint
2224
from PySide6 import QtGui
23-
from PySide6.QtGui import QGuiApplication, QPalette
25+
from PySide6.QtGui import QGuiApplication, QPalette, QTextCursor
2426
from PySide6.QtWidgets import QMessageBox, QFileDialog
2527
from avlos.definitions import RemoteAttribute, RemoteEnum, RemoteBitmask
28+
import pretty_errors
2629
import tinymovr
2730

2831

32+
class ConsoleColor:
33+
NORMAL = "<font color='white'>"
34+
ERROR = "<font color='red'>"
35+
WARNING = "<font color='orange'>"
36+
INFO = "<font color='lightblue'>"
37+
DEBUG = "<font color='lightgreen'>"
38+
TIMESTAMP = "<font color='gray'>"
39+
END = "</font>"
40+
41+
2942
app_stylesheet = """
3043
3144
/* --------------------------------------- QPushButton -----------------------------------*/
@@ -466,6 +479,67 @@ def magnitude_of(val):
466479
return val
467480

468481

482+
class StreamRedirector(object):
483+
"""
484+
A class to redirect writes from a stream to a QPlainTextEdit, including pretty_errors handling and timestamps.
485+
"""
486+
def __init__(self, widget):
487+
self.widget = widget
488+
self.buffer = ''
489+
490+
def write(self, message):
491+
# Timestamp the message
492+
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
493+
message_with_timestamp = f"[{timestamp}] {message}"
494+
495+
# Redirect to the QPlainTextEdit widget
496+
self.widget.moveCursor(QtGui.QTextCursor.End)
497+
self.widget.insertPlainText(message_with_timestamp)
498+
self.widget.ensureCursorVisible()
499+
500+
def flush(self):
501+
pass
502+
503+
504+
class QTextBrowserLogger(logging.Handler):
505+
"""A logging handler that directs logging output to a QTextBrowser widget."""
506+
def __init__(self, widget):
507+
super().__init__()
508+
self.widget = widget
509+
self.widget.setReadOnly(True)
510+
self.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
511+
512+
def emit(self, record):
513+
msg = self.format(record)
514+
self.widget.append(self.colorize_message(record.levelno, msg))
515+
516+
def colorize_message(self, level, message):
517+
color = {
518+
logging.DEBUG: "lightgreen",
519+
logging.INFO: "lightblue",
520+
logging.WARNING: "orange",
521+
logging.ERROR: "red",
522+
logging.CRITICAL: "purple"
523+
}.get(level, "black")
524+
timestamp = datetime.now().strftime('%H:%M:%S')
525+
return f'<font color="gray">[{timestamp}]</font> <font color="{color}">{message}</font>'
526+
527+
528+
def configure_pretty_errors():
529+
pretty_errors.configure(
530+
separator_character='*',
531+
filename_display=pretty_errors.FILENAME_EXTENDED,
532+
line_number_first=True,
533+
display_link=True,
534+
lines_before=5,
535+
lines_after=2,
536+
line_color=pretty_errors.RED + '> ' + pretty_errors.default_config.line_color,
537+
code_color=' ' + pretty_errors.default_config.code_color,
538+
truncate_code=True, # Truncate code lines to not overflow in the GUI
539+
display_locals=True
540+
)
541+
542+
469543
class TimedGetter:
470544
"""
471545
An interface class that maintains timing

studio/Python/tinymovr/gui/window.py

+100-11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
this program. If not, see <http://www.gnu.org/licenses/>.
1616
"""
1717

18+
import sys
1819
import time
1920
import logging
2021
import pkg_resources
@@ -28,13 +29,13 @@
2829
QMenuBar,
2930
QWidget,
3031
QFrame,
31-
QHBoxLayout,
3232
QVBoxLayout,
3333
QHeaderView,
3434
QLabel,
3535
QMessageBox,
3636
QTreeWidgetItem,
37-
QSplitter
37+
QSplitter,
38+
QTextBrowser
3839
)
3940
from PySide6.QtGui import QAction
4041
import pyqtgraph as pg
@@ -52,11 +53,14 @@
5253
Worker,
5354
PlaceholderQTreeWidget,
5455
BoolTreeWidgetItem,
56+
StreamRedirector,
57+
QTextBrowserLogger,
5558
format_value,
5659
display_file_open_dialog,
5760
display_file_save_dialog,
5861
magnitude_of,
5962
check_selected_items,
63+
configure_pretty_errors
6064
)
6165

6266

@@ -71,10 +75,8 @@ def __init__(self, app, arguments, logger):
7175
get_registry().default_format = ".6f~"
7276

7377
self.start_time = time.time()
74-
if logger is None:
75-
self.logger = logging.getLogger("tinymovr")
76-
else:
77-
self.logger = logger
78+
self.logger = logger if logger is not None else logging.getLogger("tinymovr")
79+
configure_pretty_errors()
7880

7981
self.attr_widgets_by_id = {}
8082
self.graphs_by_id = {}
@@ -86,6 +88,7 @@ def __init__(self, app, arguments, logger):
8688

8789
self.file_menu = QMenu("File")
8890
self.help_menu = QMenu("Help")
91+
self.view_menu = QMenu("View")
8992

9093
self.export_action = QAction("Export Config...", self)
9194
self.import_action = QAction("Import Config", self)
@@ -97,11 +100,19 @@ def __init__(self, app, arguments, logger):
97100
self.file_menu.addAction(self.import_action)
98101
self.help_menu.addAction(self.about_action)
99102

103+
self.toggle_tree_action = QAction("Hide Tree", self) # Assume tree is visible initially
104+
self.toggle_tree_action.triggered.connect(self.toggle_tree)
105+
self.toggle_console_action = QAction("Hide Console", self) # Assume console is visible initially
106+
self.toggle_console_action.triggered.connect(self.toggle_console)
107+
108+
self.view_menu.addAction(self.toggle_tree_action)
109+
self.view_menu.addAction(self.toggle_console_action)
110+
100111
self.menu_bar.addMenu(self.file_menu)
112+
self.menu_bar.addMenu(self.view_menu)
101113
self.menu_bar.addMenu(self.help_menu)
102114
self.setMenuBar(self.menu_bar)
103115

104-
# Setup the tree widget
105116
self.tree_widget = PlaceholderQTreeWidget()
106117
self.tree_widget.itemChanged.connect(self.item_changed)
107118
self.tree_widget.itemExpanded.connect(self.update_visible_attrs)
@@ -111,14 +122,13 @@ def __init__(self, app, arguments, logger):
111122
self.status_label = QLabel()
112123
self.status_label.setStyleSheet("margin: 5px;")
113124

114-
# Create splitter and add frames
115125
self.splitter = QSplitter(QtCore.Qt.Horizontal)
116126
self.splitter.setHandleWidth(0)
127+
self.splitter.splitterMoved.connect(self.check_tree_visibility)
117128

118129
self.left_frame = QFrame(self)
119130
self.left_layout = QVBoxLayout()
120131
self.left_layout.addWidget(self.tree_widget)
121-
self.left_layout.addWidget(self.status_label)
122132
self.left_layout.setSpacing(0)
123133
self.left_layout.setContentsMargins(0, 0, 0, 0)
124134
self.left_frame.setLayout(self.left_layout)
@@ -135,11 +145,24 @@ def __init__(self, app, arguments, logger):
135145
self.splitter.addWidget(self.left_frame)
136146
self.splitter.addWidget(self.right_frame)
137147

148+
self.console = QTextBrowser()
149+
self.console.setReadOnly(True)
150+
self.log_handler = QTextBrowserLogger(self.console)
151+
self.logger.addHandler(self.log_handler)
152+
153+
self.main_splitter = QSplitter(QtCore.Qt.Vertical)
154+
self.main_splitter.setHandleWidth(0)
155+
self.main_splitter.showEvent = self.on_show_event
156+
self.main_splitter.splitterMoved.connect(self.check_console_visibility)
157+
self.main_splitter.addWidget(self.splitter)
158+
self.main_splitter.addWidget(self.console)
159+
138160
main_layout = QVBoxLayout()
139-
main_layout.addWidget(self.splitter)
161+
main_layout.addWidget(self.main_splitter)
162+
main_layout.addWidget(self.status_label)
140163
main_layout.setSpacing(0)
141164
main_layout.setContentsMargins(0, 0, 0, 0)
142-
165+
143166
main_widget = QWidget()
144167
main_widget.setLayout(main_layout)
145168
main_widget.setMinimumHeight(600)
@@ -181,6 +204,18 @@ def __init__(self, app, arguments, logger):
181204
self.visibility_update_timer.timeout.connect(self.update_visible_attrs)
182205
self.visibility_update_timer.start(1000)
183206

207+
def on_show_event(self, event):
208+
total_height = self.splitter.size().height()
209+
top_percentage = 0.75 # 75% for the top widget
210+
bottom_percentage = 0.25 # 25% for the bottom widget
211+
212+
top_size = int(total_height * top_percentage)
213+
bottom_size = int(total_height * bottom_percentage)
214+
215+
self.main_splitter.setSizes([top_size, bottom_size])
216+
217+
super(MainWindow, self).showEvent(event)
218+
184219
@QtCore.Slot()
185220
def about_to_quit(self):
186221
self.visibility_update_timer.stop()
@@ -361,6 +396,60 @@ def show_about_box(self):
361396
),
362397
)
363398

399+
def toggle_tree(self):
400+
"""
401+
Toggle the visibility of the tree based on actual size.
402+
"""
403+
tree_size = self.splitter.sizes()[0]
404+
if tree_size > 0:
405+
self.tree_widget.setVisible(False)
406+
self.splitter.setSizes([0, self.splitter.size().width()])
407+
self.toggle_tree_action.setText("Show Tree")
408+
else:
409+
self.tree_widget.setVisible(True)
410+
total_size = self.splitter.size().width()
411+
left_size = int(total_size * 0.25)
412+
right_size = int(total_size * 0.75)
413+
self.splitter.setSizes([left_size, right_size])
414+
self.toggle_tree_action.setText("Hide Tree")
415+
416+
def toggle_console(self):
417+
"""
418+
Toggle the visibility of the console based on actual size.
419+
"""
420+
console_height = self.main_splitter.sizes()[-1]
421+
if console_height > 0:
422+
self.console.setVisible(False)
423+
self.main_splitter.setSizes([self.main_splitter.size().height(), 0])
424+
self.toggle_console_action.setText("Show Console")
425+
else:
426+
self.console.setVisible(True)
427+
total_height = self.main_splitter.size().height()
428+
top_height = int(total_height * 0.75)
429+
bottom_height = int(total_height * 0.25)
430+
self.main_splitter.setSizes([top_height, bottom_height])
431+
self.toggle_console_action.setText("Hide Console")
432+
433+
def check_tree_visibility(self, pos, index):
434+
"""
435+
Check tree visibility after splitter is moved and update the action text.
436+
"""
437+
tree_size = self.splitter.sizes()[0] # Assuming tree is always the first widget in the splitter
438+
if tree_size == 0:
439+
self.toggle_tree_action.setText("Show Tree")
440+
else:
441+
self.toggle_tree_action.setText("Hide Tree")
442+
443+
def check_console_visibility(self, pos, index):
444+
"""
445+
Check console visibility after splitter is moved and update the action text.
446+
"""
447+
console_size = self.main_splitter.sizes()[-1] # Assuming console is always the last widget in the splitter
448+
if console_size == 0:
449+
self.toggle_console_action.setText("Show Console")
450+
else:
451+
self.toggle_console_action.setText("Hide Console")
452+
364453
def is_widget_visible(self, widget):
365454
"""
366455
Check if the given widget is visible, i.e., not hidden and all its

studio/Python/tinymovr/gui/worker.py

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ def _update(self):
140140
def _device_appeared(self, device, node_id):
141141
self.mutx.lock()
142142
display_name = "{}{}".format(device.name, node_id)
143+
self.logger.info("Found {} (uid {})".format(display_name, device.uid))
143144
self.devices_by_name[display_name] = device
144145
self.names_by_id[node_id] = display_name
145146
device.name = display_name

0 commit comments

Comments
 (0)