"""
.. py:module:: pyqtmessagebar
.. moduleauthor: E.R. Uber <eruber@gmail.com>
Class PyQtMessageBar
====================
This messagebar subclasses the standard Qt QStatusbar and provides
a drop-in replacement for QStatusBar.
See the :ref:`intro_label` section for a high-level review of **PyQtMessageBar** features.
LOGGING
-------
This module creates a logging handler named 'PyQtMessageBar' and always
configures a Null handler.
See `Configuring Logging for a Library <https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library>`_.
REFERENCES
----------
The following Qt references proved helpful:
* `Qt StatusBar <https://doc.qt.io/qt-5/qstatusbar.html>`_
* `Qt Keyboard Enumerations <https://doc.qt.io/qt-5/qt.html#Key-enum>`_
LICENSE GPL
-----------
This file is part of **PyQtMessageBar**.
PyQtMessageBar 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 3 of the License, or
(at your option) any later version.
PyQtMessageBar 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.
You should have received a copy of the GNU General Public License
along with PyQtMessageBar in the file named LICENSE. If not,
see <https://www.gnu.org/licenses/>.
COPYRIGHT (C) 2020 E.R. Uber (eruber@gmail.com)
.. _pyqtmessagebar_class_label:
USAGE
-----
You use **PyQtMessageBar** in your code just like you would use QStatusBar,
it just can have a bit more going on with its constructor call. But here
we show the simplest usage::
from PyQt5.Qt import Qt
from pyqtmessagebar import PyQtMessageBar
...
# Note that the root 'self' shown here is typically an earlier
# instantiated QMainWindow
# The least complex constructor signature -- every parameter has a default value
self.statusbar = PyQtMessageBar()
# In order for keyboard input to work with a PyQtMessageBar object, the focus
# must be properly set
self.statusbar.setFocusPolicy(Qt.StrongFocus)
self.setFocusProxy(self.statusbar)
# This actually attaches the ByQtMessageBar to the QMainWindow
self.setStatusBar(statusbar)
PyQtMessageBar Details
----------------------
"""
# ----------------------------------------------------------------------------
# ------------------------ Python Standard Library ---------------------------
# ----------------------------------------------------------------------------
import os
import queue # This is a First-In-First-Out Queue
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
# ----------------------------------------------------------------------------
# -------------------------- Third Party Packages ----------------------------
# ----------------------------------------------------------------------------
from PyQt5.Qt import Qt
from PyQt5.QtCore import QEvent, QSize, QByteArray, QFileInfo, QTimer, QObject, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QIcon, QImage, QPixmap, QFont
from PyQt5.QtWidgets import (
QStatusBar, QApplication, QLabel, QFrame, QPushButton, QSizePolicy,
QFileIconProvider, QFileDialog,
)
from colour import Color
# ----------------------------------------------------------------------------
# -------------------------- Application Packages ----------------------------
# ----------------------------------------------------------------------------
from pyqtlineeditprogressbar import PyQtLineEditProgressBar
import pyqtlineeditprogressbar as PYQTPROGBAR
from pyqtmessagebar.aboutdialog import AboutDialog
from pyqtmessagebar.vline import VLine
#from pyqtmessagebar.waitqueuesignal import WaitQueueEmptiedSignal
# ----------------------------------------------------------------------------
# ----------------------- Module Global & Constants --------------------------
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# --------------------- Module Classes & Functions ---------------------------
# ----------------------------------------------------------------------------
DEFAULT_BUFFER_SIZE = 100 # Message Queue can never get smaller than this
DEFAULT_PAGE_SIZE = 10 # As used by Key_PageUp and Key_PageDown
DEFAULT_BG_COLOR_QUEUED_MSGS_WAITING = 'rgba(170,255,255,255)'
#https://doc.qt.io/qt-5/qt.html#Key-enum
KEY_MOVE = [
Qt.Key_Up, # -1 Entry
Qt.Key_Down, # +1 Entry
Qt.Key_Home, # 0th Entry
Qt.Key_End, # Max Entry
Qt.Key_PageUp, # -pagesize Entries
Qt.Key_PageDown, # +pagesize Entries
]
KEY_CMDS = [
Qt.Key_X, # Delete current message, +ALT delete all messages
Qt.Key_S, # +ALT Save queue to file
Qt.Key_L, # +ALT Load queue from file
]
KEY_MAP = KEY_MOVE + KEY_CMDS
BUILT_IN_HELP_ICON_LIGHT = 'Light'
BUILT_IN_HELP_ICON_DARK = 'Dark'
BUILT_IN_HELP_ICON_TWO_TONE = 'Two'
HELP_ICON_INDICATORS = [BUILT_IN_HELP_ICON_LIGHT, BUILT_IN_HELP_ICON_DARK, BUILT_IN_HELP_ICON_TWO_TONE]
# ----------------------------------------------------------------------------
[docs]class PyQtMessageBar(QStatusBar):
[docs] def __init__(self, parent=None,
msg_buffer_size=DEFAULT_BUFFER_SIZE,
enable_separators=False,
help_icon_file=None,
built_in_help_icon=BUILT_IN_HELP_ICON_LIGHT,
parent_logger_name=None,
save_msg_buffer_dir=None,
timer_wait_q_emptied_signal=None,
):
"""Constructor for the **PyQtMessageBar** class which subclasses QStatusBar.
See `Qt's QStatusBar Documentation <https://doc.qt.io/qt-5/qstatusbar.html>`_.
It adds a buffered message index which includes a wait queue depth and it
adds a messagebar help icon.
Parameters
----------
parent : qtwidget, optional
Reference to this widget's parent widget
msg_buffer_size : int, optional
The number of messages to buffer before removing the oldest
enable_separators : bool, optional
If True, any addPermanentWidget() calls will include a
vertical separator to the left of the widget added.
help_icon_file : str, optional
If specified, this file should be a 24x24 pixel image file
to be used to replace the built-in help icon image. If specified,
this icon will have prescedence over any built-in icon.
built_in_help_icon : str, optional
This is a string constant that can be one of three values 'Light', 'Dark', 'Two'.
Use the module constants BUILT_IN_HELP_ICON_LIGHT, BUILT_IN_HELP_ICON_DARK, or
BUILT_IN_HELP_ICON_TWO_TONE.
save_msg_buffer_dir : str, optional
A directory where any saved message buffers will be written to.
If specified as None, then saving the message buffer will be
disabled.
timer_wait_q_emptied_signal : WaitQueueEmptiedSignal object, optional
Provides a custom signal and allows user to connect their
own slot method to the timer wait queue becoming empty.
See WaitQueueEmptiedSignal :ref:`waitqueuesignal_usage_label` for
a code example of how to set this signal up.
"""
super(PyQtMessageBar, self).__init__(parent=parent)
# Constructor Parameters
self._parent = parent
self._buffer_size = msg_buffer_size
self._enable_separators = enable_separators
self._help_icon_file = help_icon_file
self._built_in_help_icon = built_in_help_icon
self._save_msg_buffer_dir = save_msg_buffer_dir
self._timer_wait_q_emptied_signal = timer_wait_q_emptied_signal
# Initializing internal data
self._progressbar_delta = None
self._timer_progressbar_update = None
self._field_width = len(str(self._buffer_size))
format_1 = "{" + "0:0{}d".format(self._field_width) + "}"
format_2 = "{" + "1:0{}d".format(self._field_width) + "}"
format_3 = " [{2:1d}]"
self._displayed_msg_idx_format = format_1 + "/" + format_2 + format_3
logger.debug("Msg Index Format Spec: '{}'".format(self._displayed_msg_idx_format))
self._displayed_msg_idx = -1 # _bufferd_msgs is empty
self._buf_page_size = DEFAULT_PAGE_SIZE
# A _bufferd_msgs entry consists of: (msg, timeout, fg, bg, bold, enqueue_time)
self._bufferd_msgs = list()
# We use a single timer, so messages that would generate overlapping timers are put on a wait queue
# until this currently displayed message timer fires.
self._timer = None
self._timer_wait_q = queue.Queue() # FIFO (First In First Out)
self._process_zero_timeout_timer = None
# This is the widget that is currently being displayed, if this is None, then
# nothing is being displayed
self._widget = None
# Intialize our user interface StatusBar and widgets a few...
self._initUI()
# -------------------------------------------------------------------------
# Private Pythonic Interface ----------------------------------------------
# -------------------------------------------------------------------------
def _initUI(self):
# This will be the default color for the countdown timer progressbar
self._progressbar_color = PYQTPROGBAR.DEFAULT_COLOR_PURPLE
# Add permanent QLabel for displayed message index
self._msg_idx_label = PyQtLineEditProgressBar(
behavior=PYQTPROGBAR.STARTS_FULL_EMPTIES_RIGHT_TO_LEFT,
progressbar_color=self._progressbar_color,
text_for_bounding_rect=" 88888/888 [88] ",
)
self._msg_idx_label.setMaxLength((2*self._field_width) + 1 + 6)
self._msg_idx_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self._clear_msg_idx_label() # rather than: self._update_msg_idx_label()
self._msg_idx_label_tt_msg = "..out of {}".format(self._buffer_size)
self._msg_idx_label.setToolTip(self._msg_idx_label_tt_msg)
self.addPermanentWidget(self._msg_idx_label)
# Add permanent Help Icon
self._help_button = QPushButton('', self)
if self._help_icon_file:
# Load Help Icon from user file
self._icon = QIcon(self._help_icon_file)
else:
# Load Help Icon from binary data
if self._built_in_help_icon == BUILT_IN_HELP_ICON_DARK:
self._icon = QIcon(':/gfx/baseline_help_black_24dp.png')
elif self._built_in_help_icon == BUILT_IN_HELP_ICON_LIGHT:
self._icon = QIcon(':/gfx/baseline_help_outline_24dp.png')
elif self._built_in_help_icon == BUILT_IN_HELP_ICON_TWO_TONE:
self._icon = QIcon(':/gfx/baseline_help_twotone_24dp.png')
else:
self._icon = QIcon(':/gfx/baseline_help_outline_24dp.png')
self._help_button.setIcon(self._icon)
self._help_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self._help_button.clicked.connect(self._statusbar_help_dialog)
self._help_button.setToolTip("Statusbar Help...")
self.addPermanentWidget(self._help_button)
self._msg_idx_label.removeProgressBar()
def _buffer_entry(self, entry_tuple):
"""The entry_tuple looks like: (msg, timeout, fg, bg, bold)"""
# Before we can update the msg index, we need to insure
# that we have not reached the limit of the buffer size
if self._displayed_msg_idx >= self._buffer_size - 1:
# Reached limit of buffer, so we delete the oldest entry
entry_to_throw_away = self._bufferd_msgs.pop(0)
logger.warning("Reached limit of buffer throwing away top entry at idx 0: {}".format(entry_to_throw_away))
# unpack the tuple
msg, timeout, fg, bg, bold = entry_tuple
# Let's add a timestamp to this entry
now = datetime.now()
timestamp = now.strftime("%Y-%m-%d %H:%M:%S")
# enqueue the message
self._bufferd_msgs.append((msg, timeout, fg, bg, bold, timestamp))
self._displayed_msg_idx = self._displayed_msg_idx + 1
logger.warning("At idx: {} Buffered: {}, to:{}, fg:{}, bg:{}, bold:{}, ts:{}".format(self._displayed_msg_idx, msg, timeout, fg, bg, bold, timestamp))
def _update_msg_idx_label(self):
"""Use the _displayed_msg_idx to update the _msg_idx_label widget"""
if self._displayed_msg_idx < 0:
# If there are no message yet displayed, the label is all dashes
#label_text = '-' * (len(str(self._buffer_size)) + 1)
self._clear_msg_idx_label()
return()
label_text = self._displayed_msg_idx_format.format(self._displayed_msg_idx, len(self._bufferd_msgs)-1,self._timer_wait_q.qsize())
#logger.debug("Updating msg index label text: {}".format(label_text))
self._msg_idx_label.setText(label_text)
#self._msg_idx_label.setAlignment(Qt.AlignCenter)
def _clear_msg_idx_label(self):
label_text = '-' * (len(str(self._buffer_size)) + 1) + '/' + '-' * (len(str(self._buffer_size)) + 1) + ' [-]'
#logger.debug("label text: {}".format(label_text))
self._msg_idx_label.setText(label_text)
self._msg_idx_label.setAlignment(Qt.AlignCenter)
def _move_viewport(self, delta):
# Enforce displayed msg idx wrapping
if len(self._bufferd_msgs) == 0:
# The queue is empty
self._displayed_msg_idx = -1
else:
# There are messages in the queue
# These are special cases for the page size commands (PAGE_UP, PAGE_DOWN)
if (self._displayed_msg_idx == 0) and (delta == -1 * self._buf_page_size):
# if we are displaying the top of the buffer and we get a PAGE_UP, go to the end of the buffer
self._displayed_msg_idx = len(self._bufferd_msgs) - 1
elif (self._displayed_msg_idx == len(self._bufferd_msgs) - 1) and (delta == self._buf_page_size):
# if we are displaying the bottom of the buffer and we get a PAGE_DOWN, go to the top of the buffer
self._displayed_msg_idx = 0
elif (self._displayed_msg_idx < self._buf_page_size) and (delta == -1 * self._buf_page_size):
# if we are displaying a location that is less than the page size away from the top of the buffer,
# and we get a PAGE_UP, go to the top of the buffer
self._displayed_msg_idx = 0
elif (self._displayed_msg_idx > len(self._bufferd_msgs) - 1 - self._buf_page_size) and \
delta == self._buf_page_size:
# if we are displaying a location that is within a page size of the end of the bugger,
# and we get a PAGE_DOWN, go to the bottom of the buffer
self._displayed_msg_idx = len(self._bufferd_msgs) - 1
else:
# Normal cases, we are moving by just one location up or down
self._displayed_msg_idx += delta
if self._displayed_msg_idx < 0:
# we are beyond the top of the buffer, wrap to end
self._displayed_msg_idx = len(self._bufferd_msgs) - 1
if self._displayed_msg_idx > len(self._bufferd_msgs) - 1:
# we are beyond the end of the buffer, wrap to beginning (home)
self._displayed_msg_idx = 0
self._display_viewport()
def _format_text(self, fg, bg, bold):
if self._widget:
if bg and fg:
self._widget.setStyleSheet("color: {}; background-color: {};".format(fg, bg))
elif bg:
self._widget.setStyleSheet("background-color: {};".format(bg))
elif fg:
self._widget.setStyleSheet("color: {};".format(fg))
if bold:
font = QFont()
font.setBold(True)
self._widget.setFont(font)
def _display_viewport(self):
self.clearMessage()
# We ignore the timeout when we are moving through the buffer
msg, timeout, fg, bg, bold, time_not_used = self._bufferd_msgs[self._displayed_msg_idx]
self._widget = QLabel(msg)
self._format_text(fg, bg, bold)
self.addWidget(self._widget, stretch=10)
self._update_msg_idx_label()
def _add_key_modifiers(self, key=None):
# https://stackoverflow.com/questions/8772595/how-to-check-if-a-keyboard-modifier-is-pressed-shift-ctrl-alt
if key:
QModifiers = QApplication.keyboardModifiers()
self._key_modifiers = []
if (QModifiers & Qt.ControlModifier) == Qt.ControlModifier:
self._key_modifiers.append('control')
if (QModifiers & Qt.AltModifier) == Qt.AltModifier:
self._key_modifiers.append('alt')
if (QModifiers & Qt.ShiftModifier) == Qt.ShiftModifier:
self._key_modifiers.append('shift')
new_key = ''
for modifier in self._key_modifiers:
new_key += modifier + '-'
new_key += key
return(new_key)
@pyqtSlot()
def _timer_wait_q_emptied(self):
print("WAIT QUEUE EMPTIED")
def _msg_timeout_fired(self, timed_msg=False):
"""Remove the widget whose timer fired"""
logger.debug("SingleShot Timer Fired")
if self._timer_progressbar_update:
self._timer_progressbar_update.stop()
# If the progressbar starts filled rather than empty,
# it will end filled, so clear it with this call.
# If the progressbar starts empty rather than filled,
# this call is superfluous.
self._msg_idx_label.removeProgressBar()
self._timer = None
if not self._timer_wait_q.empty():
msg_entry = self._timer_wait_q.get()
logger.debug("Dequeued waiting message: {}".format(msg_entry[0]))
self._buffer_this_entry(msg_entry)
if self._timer_wait_q.empty():
if self._timer_wait_q_emptied_signal:
self._timer_wait_q_emptied_signal.empty()
else:
if timed_msg:
# This was a msg with a non-zero timeout, so clear it
self.clearMessage()
def _update_progressbar(self):
self._msg_idx_label.updateProgress(self._progressbar_delta)
self._timer_progressbar_update.start(1000)
def _buffer_this_entry(self, msg_entry):
msg, timeout, fg, bg, bold = msg_entry
self.clearMessage()
self._widget = QLabel(msg)
logger.debug("Displaying & Buffering Msg: {}".format(msg))
self._format_text(fg, bg, bold)
if timeout > 0:
logger.debug("... timeout > 0...")
if self._timer is None:
logger.debug("... no timer running...")
# No currently active timer, so we set one up...
# but first lets setup the countdown progressbar if timer is long enough
if timeout > 2000: # 2 seconds
logger.debug("... timer greater than 2 seconds...")
self._progressbar_delta = 1 / (timeout/1000)
logger.info("Setting up ProgressBar Timer: {}".format(self._progressbar_delta))
self._msg_idx_label.updateProgress(self._progressbar_delta)
self._timer_progressbar_update = QTimer()
self._timer_progressbar_update.timeout.connect(self._update_progressbar)
self._timer_progressbar_update.start(1000)
self._timer = QTimer()
self._timer.singleShot(timeout, lambda: self._msg_timeout_fired(timed_msg=True))
else:
logger.debug("... there is a pending timer, so wait queue this msg")
# There is a pending timer, so put this message on the wait queue
self._timer_wait_q.put((msg, timeout, fg, bg, bold))
else:
logger.debug("... timer = 0, setting up a 1.5 second single shot timer...")
# timeout == 0
self._process_zero_timeout_timer = QTimer()
self._process_zero_timeout_timer.singleShot(1500, lambda: self._msg_timeout_fired(timed_msg=False))
self._buffer_entry((msg, timeout, fg, bg, bold))
logger.debug("...adding message widget to statusbar...")
self.addWidget(self._widget, stretch=10)
self._update_msg_idx_label()
def _enqueue_to_wait_q(self, msg, timeout, fg, bg, bold):
# if timeout == 0:
# # We need some small timeout here so the entry gets removed from the
# # wait queue by the firing of the singleShot timer
# timeout = 1000 # 1000 = 1 second
msg_entry = (msg, timeout, fg, bg, bold)
self._timer_wait_q.put(msg_entry)
self._update_msg_idx_label()
def _save_message_buffer_to_file(self, msg_buffer_file):
logger.info("Writing message buffer to file '{}'".format(msg_buffer_file))
width = len(str(len(self._bufferd_msgs))) + 1
fmt = "{" + "0:0{}d".format(width) + "}"
idx = 0
with open(msg_buffer_file, 'w') as mbf:
for entry in self._bufferd_msgs:
idx_str = fmt.format(idx)
msg, timeout, fg, bg, bold, timestamp = entry
mbf.writelines("{}: {} {} msecs FG:{} BG:{} BOLD:{} @ {}\n".format(idx_str, msg, timeout, fg, bg, bold, timestamp))
idx += 1
def _statusbar_help_dialog(self):
self.about_dialog = AboutDialog(self)
self.about_dialog.exec_()
# -------------------------------------------------------------------------
# PyQtMessageBar's Public API
# -------------------------------------------------------------------------
# Properties --------------------------------------------------------------
# @property
# def buffersize(self):
# return(self._buffer_size)
# Methods -----------------------------------------------------------------
[docs] def getWaitQueueDepth(self):
"""Returns the wait queue depth (int)"""
if self._timer_wait_q.empty():
return(0)
else:
return(self._timer_wait_q.qsize())
[docs] def waitQueueIsEmpty(self):
"""Returns True if wait queue is empty, false otherwise."""
return(self._timer_wait_q.empty())
# def setBufferSize(self, size_int):
# """Set message buffer size.
# Parameters
# ----------
# size_int : int
# This is the number of messages that can be buffered before the oldest
# message is lost
# Returns
# -------
# Nothing
# Nothing
# Note that the buffer size is never allowed to go below the default buffer size.
# Which is the constant **pyqtmessagebar.DEFAULT_BUFFER_SIZE**.
# """
# if isinstance(size_int, int):
# if size_int < DEFAULT_BUFFER_SIZE:
# size_int = DEFAULT_BUFFER_SIZE
# else:
# size_int = DEFAULT_BUFFER_SIZE
# self._buffer_size = size_int
[docs] def getBufferSize(self):
"""Returns the current message buffer size."""
return(self._buffer_size )
# def setEnableSeparators(self, flag):
# """Sets the enable separators boolean to flag value.
# The effect of this flag being True is that any
# widgets added to the statusbar via the addPermanentWidget()
# call will have a separator placed to the left of the widget added.
# """
# if isinstance(flag, bool):
# self._enable_separators = flag
[docs] def getEnableSeparators(self):
"""Returns the value of the enable separators flag."""
return(self._enable_separators)
# def setEnableDarkIcon(self, help_icon_indicator):
# """Sets the built-in help icon to the help_icon_indicator.
# Parameters
# ----------
# help_icon_indicator : str constant
# Must be one of the three following values module constants
# BUILT_IN_HELP_ICON_LIGHT, BUILT_IN_HELP_ICON_DARK, or
# BUILT_IN_HELP_ICON_TWO_TONE. The default is LIGHT.
# Note the built-in help icons can be replaced using specifying the
# **help_icon_file** parameter to the **PyQtMessageBar** constructor.
# """
# if isinstance(help_icon_indicator, str):
# if help_icon_indicator in HELP_ICON_INDICATORS:
# self._built_in_help_icon = help_icon_indicator
# else:
# self._built_in_help_icon = BUILT_IN_HELP_ICON_LIGHT
# else:
# self._built_in_help_icon = BUILT_IN_HELP_ICON_LIGHT
[docs] def getBuiltInHelpIcon(self):
"""Returns the value of the built-in help icon indicator."""
return(self._built_in_help_icon)
[docs] def setProgressBarColor(self, color_text):
"""Sets the color of the countdown timer progress bar
to the color value specified by color_text.
Note that color_text can be any color representation
supported by the `colour package <https://pypi.org/project/colour/>`_.
"""
self._msg_idx_label.setProgressBarColor(color_text)
[docs] def getProgressBarColor(self):
"""Returns the value of the countdown timer progress bar color."""
self._progressbar_color = self._msg_idx_label.getProgressBarColor()
return(self._progressbar_color)
[docs] def clearMessage(self):
"""This method added to distinguish clearing (or removing) the currently
displayed message and removing some other custom widget that the user
may have added."""
if self._widget:
self.removeWidget(self._widget)
self._widget = None
self._clear_msg_idx_label()
# -------------------------------------------------------------------------
# Here a three helper methods that serve as shorthand for setting up
# common color schemes for certain types of messages:
# showMessage(self, msg, timeout=0, fg=None, bg=None, bold=False)
# -------------------------------------------------------------------------
[docs] def showMessageError(self, msg):
"""This is a helper method that serves as a short-hand call to
**showMessage()** that configures the message to be an error message
which looks like:
Yellow FG, Brick Red BG, Bold Text, No Timeout
.. image:: _static/error_message.png
:align: center
"""
self.showMessage(msg, fg='#ffff00', bg='#aa0000', bold=True)
[docs] def showMessageWarning(self, msg):
"""This is a helper method that serves as a short-hand call to
**showMessage()** that configures the message to be an warning
message which looks like:
Black FG, Yellow BG, Bold Text, No Timeout
.. image:: _static/warning_message.png
:align: center
"""
self.showMessage(msg, fg='#000000', bg='#ffff00', bold=True)
# -------------------------------------------------------------------------
# Overriding these Qt QStatusBar methods
# -------------------------------------------------------------------------
[docs] def keyPressEvent(self, e):
"""
**Overrides Qt keyPressEvent()**
This method overrides the Qt keyPressEvent method to add keyboard input
processing. Any keys NOT processed here are passed on to the base class
implementation of keyPressEvent().
See `QWidget keyPressEvent docs <https://doc.qt.io/qt-5/qwidget.html#keyPressEvent>`_.
The following keys are recognized, all other keys are passed to the base
class implementation of keyPressEvent():
* Qt.Key_Up
* Qt.Key_Home.
* Qt.Key_Down
* Qt.Key_End
* Qt.Key_PageUp
* Qt.Key_PageDown
* control-alt-X
* control-alt-shift-X
* control-alt-S
* control-alt-shift-S
.. note:: The two key sequences based on the S key, will be disabled if the
**PyQtMessageBar** constructor is called without specifying the **save_msg_buffer_dir**
parameter.
Parameters
----------
e : QEvent
This event returns the key via the **e.key()** method call.
Returns
-------
Nothing
None, but does call the appropriate PyQtMessageBar method to handle
recognized keys.
"""
if e.type() == QEvent.KeyPress:
#print("SMARTSTATUSBAR: press {}".format(e.key()))
key = e.key()
if key in KEY_MAP:
if key in KEY_MOVE:
# https://doc.qt.io/qt-5/qt.html#Key-enum
if key == Qt.Key_Up:
#print("UP ARROW")
if self._widget:
# only if there is a widget being displayed to we decrement
# if there is no widget being displayed the idx is already
# pointing to the bottom of the buffer, so no need to decrement.
delta = -1
else:
delta = 0
if key == Qt.Key_Down:
#print("DOWN ARROW")
delta = 1
if key == Qt.Key_PageUp:
#print("PAGE UP")
delta = -1 * self._buf_page_size
if key == Qt.Key_PageDown:
#print("PAGE DOWN")
delta = self._buf_page_size
if key == Qt.Key_Home:
#print("HOME")
self._displayed_msg_idx = 0
delta = 0
if key == Qt.Key_End:
#print("END")
self._displayed_msg_idx = len(self._bufferd_msgs) - 1
delta = 0
self._move_viewport(delta)
elif key in KEY_CMDS:
if key == Qt.Key_X:
key = self._add_key_modifiers('X')
if key == 'control-alt-shift-X':
logger.debug("Deleting entire message buffer...")
self._displayed_msg_idx = -1
self._bufferd_msgs.clear()
self.clearMessage()
if key == 'control-alt-X':
entry_to_throw_away = self._bufferd_msgs.pop(self._displayed_msg_idx)
logger.debug("Deleted message @ idx {}: {}".format(self._displayed_msg_idx, entry_to_throw_away))
self._move_viewport(0)
if key == Qt.Key_S:
if self._save_msg_buffer_dir:
key = self._add_key_modifiers('S')
if key == 'control-alt-shift-S':
msg_buffer_file = datetime.now().strftime("%Y-%m-%d-%Hh%Mm%Ss%f") + '.msgs'
start_file_name = os.path.join(self._save_msg_buffer_dir, msg_buffer_file)
msg_buffer_file, _ = QFileDialog.getSaveFileName(self, 'Save Message Buffer File',
start_file_name, "Msg Buf Files (*.msgs)")
self._save_message_buffer_to_file(msg_buffer_file)
if key == 'control-alt-S':
msg_buffer_file = os.path.join(self._save_msg_buffer_dir, datetime.now().strftime("%Y-%m-%d-%Hh%Mm%Ss%f") + '.msgs')
self._save_message_buffer_to_file(msg_buffer_file)
else:
# If we do not act on the key, then we need to call the base class's
# implementation of keyPressEvent() so some other widget may act on it.
super(PyQtMessageBar, self).keyPressEvent(e)
[docs] def currentMessage(self):
"""**Overrides QStatusBar.currentMessage()**
Returns the currently displayed message or the empty string if
there is no currently dislayed message."""
if self._widget:
msg = self._widget.text()
else:
msg = ''
return(msg)
[docs] def showMessage(self, msg, timeout=0, fg=None, bg=None, bold=False):
"""**Overrides QStatusBar.showMessage()**
This method completely replaces Qt's QStatusBar.ShowMessage()
because we need inner knowledge of when messages timeout.
We also add colors for foreground (fg), background (bg), and
a flag for enabling bold text.
Note, we do not provide the stretch parameter because we control
the layout, at least we think we do. :)
Parameters
----------
msg : str
The statusbar message to be displayed and buffered.
timeout : int
If non-zero the message will have a timeout and be removed
from the statusbar display once the timeout expires.
fg : str (color)
The foreground color (text color) of the message to be displayed.
The color value can be any color representation supported by
the `colour package <https://pypi.org/project/colour/>`_.
If not specified, the system default color is used.
bg : str (color)
The background color of the message to be displayed.
The color value can be any color representation supported by
the `colour package <https://pypi.org/project/colour/>`_.
If not specified, the system default color is used.
bold: bool
If True the text of the message will be bold.
"""
# pad msg with a leading space...
msg = ' ' + msg
logger.debug("showMessage called...")
if self._widget:
logger.debug("msg widget already being displayed...")
# There is a msg being displayed...
# If there are other pending messages that have not yet been displayed,
# we enqueue this message to the wait queue.
# If msg being displayed has no timeout, we clear it and show this msg.
if self._timer:
logger.debug("Wait Queueing message: '{}'".format(msg))
# We are waiting the currently diplayed message to timeout,
# or we have older pending messages that have not yet been displayed;
# so put this message on the wait queue
self._enqueue_to_wait_q(msg, timeout, fg, bg, bold)
return()
else:
logger.debug("No wait Q timer running...")
# No message being displayed, however, we may have previous message in the wait queue
# being processed by the singleShot Timer; only if the wait queue is empty do we
# process the caller's message; otherwise, we put it on the wait queue.
if self._timer_wait_q.empty():
logger.debug("Wait Q empty, clear displayed message and buffer new msg.")
# self.clearMessage() -->> This is called first thing in _buffer_this_entry() below
self._buffer_this_entry((msg, timeout, fg, bg, bold))
else:
logger.debug("Wait Q NOT empty, enqueue current message...")
self._enqueue_to_wait_q(msg, timeout, fg, bg, bold)