Beispiel #1
0
    def __init__(self) -> None:
        super().__init__()

        self._handle = TranslateToolHandle.TranslateToolHandle(
        )  #type: TranslateToolHandle.TranslateToolHandle #Because for some reason MyPy thinks this variable contains Optional[ToolHandle].
        self._enabled_axis = [
            ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis
        ]

        self._grid_snap = False
        self._grid_size = 10
        self._moved = False

        self._shortcut_key = Qt.Key.Key_T

        self._distance_update_time = None  #type: Optional[float]
        self._distance = None  #type: Optional[Vector]

        self.setExposedProperties("ToolHint", "X", "Y", "Z",
                                  SceneNodeSettings.LockPosition)

        self._update_selection_center_timer = QTimer()
        self._update_selection_center_timer.setInterval(50)
        self._update_selection_center_timer.setSingleShot(True)
        self._update_selection_center_timer.timeout.connect(
            self.propertyChanged.emit)

        # Ensure that the properties (X, Y & Z) are updated whenever the selection center is changed.
        Selection.selectionCenterChanged.connect(
            self._onSelectionCenterChanged)

        # CURA-5966 Make sure to render whenever objects get selected/deselected.
        Selection.selectionChanged.connect(self.propertyChanged)
 def __init__(self, *args, **kwargs):
     super(ProgressBar, self).__init__(*args, **kwargs)
     self.setValue(0)
     if self.minimum() != self.maximum():
         self.timer = QTimer(self)
         self.timer.timeout.connect(self.onTimeout)
         self.timer.start(100)
    def __init__(self, parent=None) -> None:
        super().__init__(parent)
        self.addRoleName(self.NameRole, "name")
        self.addRoleName(self.IdRole, "id")
        self.addRoleName(self.MetaDataRole, "metadata")
        self.addRoleName(self.ReadOnlyRole, "readOnly")
        self.addRoleName(self.SectionRole, "section")

        #We keep track of two sets: One for normal containers that are already fully loaded, and one for containers of which only metadata is known.
        #Both of these are indexed by their container ID.
        self._instance_containers = {}  #type: Dict[str, InstanceContainer]
        self._instance_containers_metadata = {
        }  # type: Dict[str, Dict[str, Any]]

        self._section_property = ""

        # Listen to changes
        ContainerRegistry.getInstance().containerAdded.connect(
            self._onContainerChanged)
        ContainerRegistry.getInstance().containerRemoved.connect(
            self._onContainerChanged)
        ContainerRegistry.getInstance().containerLoadComplete.connect(
            self._onContainerLoadComplete)

        self._container_change_timer = QTimer()
        self._container_change_timer.setInterval(150)
        self._container_change_timer.setSingleShot(True)
        self._container_change_timer.timeout.connect(self._update)

        # List of filters for queries. The result is the union of the each list of results.
        self._filter_dicts = []  # type: List[Dict[str, str]]
        self._container_change_timer.start()
Beispiel #4
0
 def createTimer(self):
     self.timer = QTimer()
     self.timer.timeout.connect(self.updateTime)
     self.timer.timeout.connect(self.maybeChangeMode)
     self.timer.setInterval(1000)
     self.timer.setSingleShot(False)
     self.timer.start()
Beispiel #5
0
 def close_tab(self, tab=None):
     tab = tab or self.current_tab
     if tab is not None:
         self.delete_removed_tabs(self.tab_tree.remove_tab(tab))
     if not self.tabs:
         self.open_url(WELCOME_URL, switch_to_tab=True)
         QTimer.singleShot(0, self.current_tab_changed)
class PlayerLabel(QLabel):
    loading_movie = None

    def __init__(self, fontsize, parent=None):
        super().__init__(parent)
        self.fontsize = fontsize

        cls = type(self)
        if not cls.loading_movie:
            cls.loading_movie = QMovie(resource_path("loading.gif"))
            cls.loading_movie.setScaledSize(QSize(MOVIEWIDTH, MOVIEWIDTH))
            cls.loading_movie.start()
        # self.player_heading.setGeometry(0, 140, self.rect().width(), 50)
        # self.player_heading.setAlignment(Qt.AlignmentFlag.AlignHCenter)
        f = self.font()
        f.setPointSize(self.fontsize)
        self.setFont(f)
        self.setAutoFillBackground(True)

        self.setMovie(cls.loading_movie)

        self.blink_timer = None

    def buzz_hint(self):
        self.setStyleSheet("QLabel { background-color : grey}")
        self.blink_timer = QTimer()
        # self.blink_timer.moveToThread(QApplication.instance().thread())
        self.blink_timer.timeout.connect(self._buzz_hint_callback)
        self.blink_timer.start(100)

    def _buzz_hint_callback(self):
        self.setStyleSheet("QLabel { background-color : none}")
    def restart(self):
        self.player_view.close()
        self.player_view = PlayerView(self.rect() - QMargins(0, 210, 0, 0),
                                      parent=self)
        # self.show_overlay()
        QTimer.singleShot(500, self.show_overlay)

        self.startButton.setEnabled(False)
Beispiel #8
0
 def __init__(self, size=10 * 1024 * 1024):
     QObject.__init__(self)
     self._state = QWebEngineDownloadRequest.DownloadState.DownloadRequested
     self._received = self._total = -1
     FakeDownloadItem.idc += 1
     self._id = FakeDownloadItem.idc
     self._size = size
     self.fname = '%s.epub' % self.id()
     self.mimeType = lambda: mimetypes.guess_type(self.fname)[0]
     QTimer.singleShot(100, self._tick)
Beispiel #9
0
    def showMessage(self, message: Message) -> None:
        with self._message_lock:
            if message not in self._visible_messages:
                self._visible_messages.append(message)
                message.setLifetimeTimer(QTimer())
                message.setInactivityTimer(QTimer())
                self.visibleMessageAdded.emit(message)

        # also show toast message when the main window is minimized
        self.showToastMessage(self._app_name, message.getText())
Beispiel #10
0
    def start_update_timer(self, mins: int = 180) -> None:
        """Check for updates every 3 hrs"""
        if not cf.SYS_FROZEN:
            return

        msec = mins * 60 * 1000

        self.update_timer = QTimer(parent=self)
        self.update_timer.timeout.connect(self.check_update)
        self.update_timer.start(msec)
    def __init__(
        self,
        parent=None,
        buttons=None,
        exercises=None,
        index: int = None,
    ):
        super(ChangeKeyDialog, self).__init__(parent)
        layout = QVBoxLayout(self)
        self.setLayout(layout)
        widget = QWidget()
        keyLayout = QVBoxLayout()
        widget.setStyleSheet("""
        QWidget{
            border-radius: 12px;
            border: 1px solid grey;
            background-color: #b5b5b5;
            color: white;
            font-size: 40px;
        }
        """)
        # widget.setFixedSize(100, 100)
        self.currentKeyLabel = QLabel('W')
        keyLayout.addWidget(self.currentKeyLabel)
        keyLayout.setAlignment(self.currentKeyLabel, Qt.Alignment.AlignCenter)
        widget.setLayout(keyLayout)

        label = QLabel("Press a key to swap")
        emptyKey = QPushButton('Use empty slot')
        emptyKey.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
        emptyKey.clicked.connect(self.useEmpty)

        acceptKey = QPushButton('Accept')
        acceptKey.clicked.connect(self.accept)
        acceptKey.setFocusPolicy(Qt.FocusPolicy.ClickFocus)

        layout.addWidget(label)
        layout.addWidget(widget)
        actions = QHBoxLayout()
        actions.addWidget(emptyKey)
        actions.addWidget(acceptKey)
        layout.addLayout(actions)
        layout.setAlignment(widget, Qt.Alignment.AlignCenter)
        self.buttons = buttons
        self.exercises = exercises
        self.index = index

        self.monitor = KeyMonitor()
        self.monitor.start_monitoring()
        self.currentKey = self.monitor.currentKey

        self.timer = QTimer()
        self.timer.timeout.connect(self.onTimeout)
        self.timer.start()
        print("Dialog init done!")
Beispiel #12
0
    def __init__(self,
                 request_id: str,
                 http_method: str,
                 request: "QNetworkRequest",
                 manager_timeout_callback: Callable[["HttpRequestData"], None],
                 data: Optional[Union[bytes, bytearray]] = None,
                 callback: Optional[Callable[["QNetworkReply"], None]] = None,
                 error_callback: Optional[
                     Callable[["QNetworkReply", "QNetworkReply.NetworkError"],
                              None]] = None,
                 download_progress_callback: Optional[Callable[[int, int],
                                                               None]] = None,
                 upload_progress_callback: Optional[Callable[[int, int],
                                                             None]] = None,
                 timeout: Optional[float] = None,
                 reply: Optional["QNetworkReply"] = None,
                 parent: Optional["QObject"] = None) -> None:
        super().__init__(parent=parent)

        # Sanity checks
        if timeout is not None and timeout <= 0:
            raise ValueError(
                "Timeout must be a positive value, but got [%s] instead." %
                timeout)

        self._request_id = request_id
        self.http_method = http_method
        self.request = request
        self.data = data
        self.callback = callback
        self.error_callback = error_callback
        self.download_progress_callback = download_progress_callback
        self.upload_progress_callback = upload_progress_callback
        self._timeout = timeout
        self.reply = reply

        # For benchmarking. For calculating the time a request spent pending.
        self._create_time = time.time()

        # The timestamp when this request was initially issued to the QNetworkManager. This field to used to track and
        # manage timeouts (if set) for the requests.
        self._start_time = None  # type: Optional[float]
        self.is_aborted_due_to_timeout = False

        self._last_response_time = float(0)
        self._timeout_timer = QTimer(parent=self)
        if self._timeout is not None:
            self._timeout_timer.setSingleShot(True)
            timeout_check_interval = int(self._timeout * 1000 *
                                         (1 + self.TIMEOUT_CHECK_TOLERANCE))
            self._timeout_timer.setInterval(timeout_check_interval)
            self._timeout_timer.timeout.connect(self._onTimeoutTimerTriggered)

        self._manager_timeout_callback = manager_timeout_callback
Beispiel #13
0
 def __init__(self):
     self._clock_fmt = '%Y-%m-%d %H:%M:%S %Z%z'
     self._tz_utc = pytz.timezone('UTC')
     self._tz_eastern = pytz.timezone('US/Eastern')
     self._tz_central = pytz.timezone('US/Central')
     self._tz_mountain = pytz.timezone('US/Mountain')
     self._tz_pacific = pytz.timezone('US/Pacific')
     self._tz_berlin = pytz.timezone('Europe/Berlin')
     self._tz_london = pytz.timezone('Europe/London')
     self._tz_paris = pytz.timezone('Europe/Paris')
     self._timer = QTimer()
     self._timer.timeout.connect(self._show_clock)
Beispiel #14
0
class KeysWidget(QWidget):
    keyPressed = QtCore.pyqtSignal(QtCore.QEvent)

    def __init__(self, parent=None):
        super(KeysWidget, self).__init__(parent)
        self.classifyExercises = None
        if parent is not None:
            self.classifyExercises = parent.classifyExercises
            self.infoLabel = parent.infoLabel

        self.ui = Ui_KeysPanel()
        self.ui.setupUi(self)
        self.monitor = KeyMonitor()
        self.monitor.start_monitoring()

        self.timer = QTimer()
        self.timer.timeout.connect(self.onTimeout)
        self.timer.start()

        self.ui.saveProfile.clicked.connect(self.saveBindings)

    def saveBindings(self):
        if self.classifyExercises.subject is not None:
            key_list = [x.serialize() for x in self.classifyExercises.exercises.values()]
            content = {
                self.classifyExercises.subject: key_list
            }
            print(content)
            with open(MAPPED_KEYS_PATH + self.classifyExercises.subject + '.json', "w") as f:
                json.dump(content, f)

    def onTimeout(self):
        if self.monitor.released:
            for b in self.ui.buttons:
                b.setStyleSheet(
                    """ QPushButton
                    {
                        border: 1px solid grey;
                        background-color: white;
                    }
                    """)
        else:
            for ind in range(0, len(self.ui.exercises)):
                if self.monitor.currentKey == self.ui.exercises[ind].assigned_key[1]:
                    self.ui.buttons[ind].setStyleSheet(
                    """ QPushButton
                    {
                        border: 1px solid green;
                        background-color: #7FFFD4;
                    }
                    """)
Beispiel #15
0
 def call_auto_get_cookie(self):
     """自动读取浏览器cookie槽函数"""
     try:
         self._cookie = get_cookie_from_browser()
     except Exception as e:
         logger.error(f"Browser_cookie3 Error: {e}")
         self.auto_get_cookie_ok.setPlainText(f"❌获取失败,错误信息\n{e}")
     else:
         if self._cookie:
             self._user = self._pwd = ''
             self.auto_get_cookie_ok.setPlainText("✅获取成功即将登录……")
             QTimer.singleShot(2000, self._close_dialog)
         else:
             self.auto_get_cookie_ok.setPlainText("❌获取失败\n请提前使用支持的浏览器登录蓝奏云,读取前完全退出浏览器!\n支持的浏览器与顺序:\nchrome, chromium, opera, edge, firefox")
Beispiel #16
0
    def __init__(self, output_controller: PrinterOutputController) -> None:
        """Constructor.

    Args:
      output_controller: Printer's output controller.
    """
        super().__init__(output_controller=output_controller, key='', name='')
        self._state = 'not_started'
        self._progress = 0  # type: int
        self._elapsed_print_time_millis = 0  # type: int
        self._elapsed_percentage_points = None  # type: Optional[int]
        # Estimated printing time left, in seconds.
        self._remaining_print_time_secs = _MAX_REMAINING_TIME_SECS
        self._stopwatch = QTimer(self)
        self._stopwatch.timeout.connect(self._tick)
        self._reset()
Beispiel #17
0
def run_app(urls=(),
            callback=None,
            callback_wait=0,
            master_password=None,
            new_instance=False,
            shutdown=False,
            restart_state=None,
            no_session=False,
            startup_session=None):
    env = os.environ.copy()
    app = Application(master_password=master_password,
                      urls=urls,
                      new_instance=new_instance,
                      shutdown=shutdown,
                      restart_state=restart_state,
                      no_session=no_session)
    os.environ['QTWEBENGINE_DICTIONARIES_PATH'] = os.path.join(
        config_dir, 'spell')
    original_env = env
    style = Style()
    app.setStyle(style)
    try:
        if startup_session is not None:
            with open(startup_session, 'rb') as f:
                app.unserialize_state(pickle.load(f))
        elif restart_state is not None:
            app.unserialize_state(restart_state)
        else:
            last_session = last_saved_session(no_session)
            if last_session is None or urls:
                app.open_urls(urls)
            else:
                app.unserialize_state(last_session)
        if callback is not None:
            QTimer.singleShot(callback_wait, callback)
        app.exec()
    finally:
        app.break_cycles()
        delete_profile()
        places.close()
        app.sendPostedEvents()
        restart_state = getattr(app, 'restart_state', None)
        sip.delete(app)
        del app
        gc.collect(), gc.collect(), gc.collect()
        if restart_state is not None:
            restart(restart_state, original_env)
Beispiel #18
0
    def __init__(self, parent=None):
        super(KeysWidget, self).__init__(parent)
        self.classifyExercises = None
        if parent is not None:
            self.classifyExercises = parent.classifyExercises
            self.infoLabel = parent.infoLabel

        self.ui = Ui_KeysPanel()
        self.ui.setupUi(self)
        self.monitor = KeyMonitor()
        self.monitor.start_monitoring()

        self.timer = QTimer()
        self.timer.timeout.connect(self.onTimeout)
        self.timer.start()

        self.ui.saveProfile.clicked.connect(self.saveBindings)
Beispiel #19
0
    def open_devtools_tab(self, web_page):
        ''' Open devtools tab'''
        self.devtools_page = web_page
        eval_in_emacs('eaf-open-devtool-page', [])

        # We need adjust web window size after open developer tool.
        QTimer().singleShot(
            1000,
            lambda: eval_in_emacs('eaf-monitor-configuration-change', []))
Beispiel #20
0
 def _tick(self):
     if self._state not in (QWebEngineDownloadRequest.DownloadState.DownloadInProgress, QWebEngineDownloadRequest.DownloadState.DownloadRequested):
         return
     if self._total == -1:
         self._total = self._size
         self._received = 0
         self._state = QWebEngineDownloadRequest.DownloadState.DownloadInProgress
         self.stateChanged.emit(self._state)
         self.downloadProgress.emit(self._received, self._total)
         QTimer.singleShot(100, self._tick)
     elif self._received < self._total:
         self._received += min(self._total - self._received, self._size // 100)
         self.downloadProgress.emit(self._received, self._total)
         if self._received >= self._total:
             self._state = QWebEngineDownloadRequest.DownloadState.DownloadCompleted
             self.stateChanged.emit(self._state)
             self.finished.emit()
         else:
             QTimer.singleShot(100, self._tick)
Beispiel #21
0
class WinForm(QWidget):
    def __init__(self, parent=None):
        super(WinForm, self).__init__(parent)
        self.setWindowTitle("QTimer demo")
        self.listFile = QListWidget()
        self.label = QLabel("显示当前时间")
        self.startButton = QPushButton("开始")
        self.endButton = QPushButton("结束")
        layout = QGridLayout(self)

        # 初始化定时器
        self.timer = QTimer(self)
        # 显示时间
        self.timer.timeout.connect(
            self.showTime)  # timeout 信号连接到特定的槽,当定时器超时,发出 timeout 信号

        layout.addWidget(self.label, 0, 0, 1, 2)
        layout.addWidget(self.startButton, 1, 0)
        layout.addWidget(self.endButton, 1, 1)

        self.startButton.clicked.connect(self.start_timer)
        self.endButton.clicked.connect(self.end_timer)

        self.setLayout(layout)

    def showTime(self):
        # 获取当前系统时间
        time = QDateTime.currentDateTime()
        # 设置时间格式
        timeDisplay = time.toString("yyyy-MM-dd hh:mm:ss dddd")
        self.label.setText(timeDisplay)

    def start_timer(self):
        # 设置时间间隔并启动定时器
        self.timer.start(1000)  # start 内设置时间间隔,启动或重新启动计时器,如果计时器在运行,则重启
        self.startButton.setEnabled(False)
        self.endButton.setEnabled(True)

    def end_timer(self):
        self.timer.stop()  # 停止计时器
        self.startButton.setEnabled(True)
        self.endButton.setEnabled(False)
    def __init__(self, parent=None) -> None:
        super().__init__(parent=parent)

        self._property_map = QQmlPropertyMap(self)

        self._stack = None  # type: Optional[ContainerStack]
        self._key = ""
        self._relations = set()  # type: Set[str]
        self._watched_properties = []  # type: List[str]
        self._store_index = 0
        self._value_used = None  # type: Optional[bool]
        self._stack_levels = []  # type: List[int]
        self._remove_unused_value = True
        self._validator = None  # type: Optional[Validator]

        self._update_timer = QTimer(self)
        self._update_timer.setInterval(100)
        self._update_timer.setSingleShot(True)
        self._update_timer.timeout.connect(self._update)

        self.storeIndexChanged.connect(self._storeIndexChanged)
class ProgressBar(QProgressBar):
    def __init__(self, *args, **kwargs):
        super(ProgressBar, self).__init__(*args, **kwargs)
        self.setValue(0)
        if self.minimum() != self.maximum():
            self.timer = QTimer(self)
            self.timer.timeout.connect(self.onTimeout)
            self.timer.start(100)

    def start(self):
        self.timer.start(100)

    def stop(self):
        self.timer.stop()

    def onTimeout(self):
        if self.value() >= 100:
            self.timer.stop()
            self.timer.deleteLater()
            del self.timer
            return
        self.setValue(self.value() + 1)
Beispiel #24
0
    def _scheduleDelayedCallEvent(self, event: "_CallFunctionEvent") -> None:
        if event.delay is None:
            return

        timer = QTimer(self)
        timer.setSingleShot(True)
        timer.setInterval(event.delay * 1000 * (1 + self.TIME_TOLERANCE))
        timer_callback = lambda e=event: self._onDelayReached(e)
        timer.timeout.connect(timer_callback)
        timer.start()
        self._delayed_events[event] = {
            "event": event,
            "timer": timer,
            "timer_callback": timer_callback,
        }
Beispiel #25
0
    def __init__(self, parent=None):
        super(WinForm, self).__init__(parent)
        self.setWindowTitle("QTimer demo")
        self.listFile = QListWidget()
        self.label = QLabel("显示当前时间")
        self.startButton = QPushButton("开始")
        self.endButton = QPushButton("结束")
        layout = QGridLayout(self)

        # 初始化定时器
        self.timer = QTimer(self)
        # 显示时间
        self.timer.timeout.connect(
            self.showTime)  # timeout 信号连接到特定的槽,当定时器超时,发出 timeout 信号

        layout.addWidget(self.label, 0, 0, 1, 2)
        layout.addWidget(self.startButton, 1, 0)
        layout.addWidget(self.endButton, 1, 1)

        self.startButton.clicked.connect(self.start_timer)
        self.endButton.clicked.connect(self.end_timer)

        self.setLayout(layout)
Beispiel #26
0
    def __init__(self, on_cancelled: Callable) -> None:
        """Constructor.

    Args:
      on_cancelled: Called when user cancels printer upload.
    """
        super().__init__(title=I18N_CATALOG.i18nc(
            '@info:status', 'Uploading model to printer'),
                         text=self.CALCULATING_TEXT,
                         progress=-1,
                         lifetime=0,
                         dismissable=False,
                         use_inactivity_timer=False)
        self._on_cancelled = on_cancelled
        self._elapsed_upload_time_millis = 0
        self._remaining_time_millis = self.MAX_REMAINING_MILLIS
        self.addAction('cancel', I18N_CATALOG.i18nc('@action:button',
                                                    'Cancel'), 'cancel',
                       I18N_CATALOG.i18nc('@action', 'Cancels job upload.'))
        self.actionTriggered.connect(self._on_action_triggered)
        self._stopwatch = QTimer(self)
        self._stopwatch.timeout.connect(self._tick)
        self._reset_calculation_time()
Beispiel #27
0
 def __init__(self, parent):
     QStackedWidget.__init__(self, parent)
     self.permanent_message = ''
     self.temporary_message = TemporaryMessage(1, '', 'info', monotonic())
     self.msg = Message(self, parent.sb_background)
     self.addWidget(self.msg)
     self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
     self.msg.setFocusPolicy(Qt.FocusPolicy.NoFocus)
     self.search = SearchPanel(self)
     self.search.edit.abort_search.connect(self.hide_search)
     self.search.edit.editingFinished.connect(self.hide_search)
     self.addWidget(self.search)
     self.update_timer = t = QTimer(self)
     self.fg_color = color('status bar foreground', None)
     if self.fg_color:
         self.fg_color = QColor(self.fg_color)
     t.setSingleShot(True), t.setInterval(100), t.timeout.connect(
         self.update_message)
Beispiel #28
0
class QDoublePushButton(QPushButton):
    """加入了双击事件的按钮"""
    doubleClicked = pyqtSignal()
    clicked = pyqtSignal()

    def __init__(self, *args, **kwargs):
        QPushButton.__init__(self, *args, **kwargs)
        self.timer = QTimer()
        self.timer.setSingleShot(True)
        self.timer.timeout.connect(self.clicked.emit)
        super().clicked.connect(self.checkDoubleClick)

    def checkDoubleClick(self):
        if self.timer.isActive():
            self.doubleClicked.emit()
            self.timer.stop()
        else:
            self.timer.start(250)
Beispiel #29
0
 def __init__(self,
              icon_name='busy.svg',
              icon_size=24,
              duration=2,
              frames=120,
              parent=None):
     QObject.__init__(self, parent)
     pmap = get_icon(icon_name).pixmap(icon_size, icon_size)
     self.interval = duration * 1000 // frames
     self.timer = t = QTimer(self)
     t.setInterval(self.interval)
     t.timeout.connect(self.do_update)
     self.frame_number = 0
     self.frames = []
     angle_delta = 360 / frames
     angle = -angle_delta
     for i in range(frames):
         angle += angle_delta
         p = pmap
         if angle:
             p = self.rotated_by(pmap, angle)
         self.frames.append(p)
Beispiel #30
0
class MainWindow(QMainWindow):
    """Main application window"""
    minesite_changed = pyqtSignal(str)

    def __init__(self):
        super().__init__()
        self.app = QApplication.instance()
        self.setWindowTitle(gbl.title)
        self.setMinimumSize(QSize(1000, 400))
        self.minesite_changed.connect(self.update_minesite_label)
        self.minesite_label = QLabel(
            self
        )  # permanent label for status bar so it isnt changed by statusTips
        self.minesite_label.setToolTip(
            'Global MineSite > Set with [Ctrl + Shift + M]')
        self.rows_label = QLabel(self)
        self.statusBar().addPermanentWidget(self.rows_label)
        self.statusBar().addPermanentWidget(self.minesite_label)

        # Settings
        s = QSettings('sms', 'smseventlog', self)

        screen_point = s.value('window position', False)
        screen_size = s.value('window size', False)

        # if screen size/left anchor pt values are not set or out of range, use default
        if not (screen_point and screen_size
                and gbl.check_screen_point(screen_point)):
            screen_point = QPoint(50, 50)
            screen_size = QSize(1200, 1000)

        # move/resize MainWindow to last position/size
        self.resize(screen_size)
        self.move(screen_point)
        self.settings = s

        self.menus = {}
        self.create_actions()

        self.tabs = TabWidget(self)
        self.setCentralWidget(self.tabs)
        self.update_minesite_label()

        self.threadpool = QThreadPool(self)
        log.debug('Mainwindow init finished.')

    @property
    def minesite(self) -> str:
        """Global minesite setting"""
        return self.settings.value('minesite', defaultValue='FortHills')
        # return self._minesite

    @minesite.setter
    def minesite(self, val: Any) -> None:
        """Save minesite back to settings"""
        # self._minesite = val
        self.settings.setValue('minesite', val)
        self.minesite_changed.emit(val)

    def update_minesite_label(self, *args):
        """minesite_label is special label to always show current minesite (bottom right)"""
        self.minesite_label.setText(f'Minesite: {self.minesite}')

    def update_rows_label(self, *args):
        view = self.active_table()
        if view is None:
            return  # not init yet

        model = view.data_model
        visible_rows = model.visible_rows
        total_rows = model.total_rows

        if total_rows == visible_rows:
            num_rows = visible_rows
        else:
            num_rows = f'{visible_rows}/{total_rows}'

        self.rows_label.setText(f'Rows: {num_rows}')

    def warn_not_implemented(self) -> None:
        """Let user know feature not implemented"""
        self.update_statusbar('Warning: This feature not yet implemented.')

    def update_statusbar(self,
                         msg: str = None,
                         warn: bool = False,
                         success: bool = False,
                         log_: bool = False,
                         *args) -> None:
        """Statusbar shows temporary messages that disappear on any context event"""
        if not msg is None:

            # allow warn or success status to be passed with msg as dict
            if isinstance(msg, dict):
                warn = msg.get('warn', False)
                success = msg.get('success', False)
                msg = msg.get('msg', None)  # kinda sketch

            if log_:
                log.info(msg)

            bar = self.statusBar()
            self.prev_status = bar.currentMessage()
            bar.showMessage(msg)

            msg_lower = msg.lower()
            if warn or 'warn' in msg_lower or 'error' in msg_lower:
                color = '#ff5454'  # '#fa7070'
            elif success or 'success' in msg_lower:
                color = '#70ff94'
            else:
                color = 'white'

            palette = bar.palette()
            palette.setColor(QPalette.ColorRole.WindowText, QColor(color))
            bar.setPalette(palette)

            self.app.processEvents()

    def revert_status(self):
        # revert statusbar to previous status
        if not hasattr(self, 'prev_status'):
            self.prev_status = ''

        self.update_statusbar(msg=self.prev_status)

    @er.errlog()
    def after_init(self):
        """Steps to run before MainWindow is shown.
        - Everything in here must suppress errors and continue
        """
        self.username = self.get_username()
        self.init_sentry()

        self.u = users.User(username=self.username, mainwindow=self).login()
        log.debug('user init')

        last_tab_name = self.settings.value('active table', 'Event Log')
        self.tabs.init_tabs()
        self.tabs.activate_tab(title=last_tab_name)
        log.debug('last tab activated')

        # initialize updater
        self.updater = Updater(mw=self,
                               dev_channel=self.get_setting('dev_channel'))
        log.debug(f'updater initialized, channel={self.updater.channel}')

        t = self.active_table_widget()
        if t.refresh_on_init:
            t.refresh(default=True, save_query=False)
            log.debug('last table refreshed')

        # startup update checks can allow ignoring dismissed versions
        self.check_update(allow_dismissed=True)
        self.start_update_timer()
        log.debug('Finished after_init')

    def start_update_timer(self, mins: int = 180) -> None:
        """Check for updates every 3 hrs"""
        if not cf.SYS_FROZEN:
            return

        msec = mins * 60 * 1000

        self.update_timer = QTimer(parent=self)
        self.update_timer.timeout.connect(self.check_update)
        self.update_timer.start(msec)

    @er.errlog('Failed to check for update!', display=True)
    def check_update(self, allow_dismissed: bool = False, *args):
        """Check for update and download in a worker thread
        """
        if not cf.SYS_FROZEN:
            self.update_statusbar('App not frozen, not checking for updates.')
            return

        if self.updater.update_available:
            # update has been previously checked and downloaded but user declined to install initially
            self._install_update(updater=self.updater,
                                 allow_dismissed=allow_dismissed)
        else:
            Worker(func=self.updater.check_update, mw=self) \
                .add_signals(signals=(
                    'result',
                    dict(func=lambda updater: self._install_update(updater, allow_dismissed=allow_dismissed)))) \
                .start()

    def _install_update(self,
                        updater: Updater = None,
                        ask_user: bool = True,
                        allow_dismissed: bool = False) -> None:
        """Ask if user wants to update and show changelog

        Parameters
        ----------
        updater : Updater, optional
            Updater obj, default None
        ask_user : bool, optional
            prompt user to update or just install, default True
        allow_dismissed : bool, optional
            allow ignoring patch updates if user has dismissed once
        """

        # update check failed, None result from thread
        if updater is None:
            return

        v_current = updater.version
        v_latest = updater.ver_latest

        # check if PATCH update has been dismissed
        if not updater.needs_update and allow_dismissed:
            log.info('User declined current update. current:' +
                     f'{v_latest}, dismissed: {updater.ver_dismissed}')
            return

        # show changelog between current installed and latest version
        markdown_msg = updater.get_changelog_new()

        # prompt user to install update and restart
        msg = 'An updated version of the Event Log is available.\n\n' \
            + f'Current: {v_current}\n' \
            + f'Latest: {v_latest}\n\n' \
            + 'Would you like to restart and update now?' \
            + '\n\nNOTE - Patch updates (eg x.x.1) can be dismissed. Use Help > Check for Update ' \
            + 'to prompt again.'

        if ask_user:
            if not dlgs.msgbox(msg=msg, yesno=True, markdown_msg=markdown_msg):

                # mark version as dismissed
                self.settings.setValue('ver_dismissed', str(v_latest))
                self.update_statusbar(
                    f'User dismissed update version: {v_latest}', log_=True)
                return

        Worker(func=updater.install_update, mw=self).start()
        self.update_statusbar('Extracting update and restarting...')

    def show_full_changelog(self) -> None:
        """Show full changelog"""
        msg = self.updater.get_changelog_full()
        dlgs.msgbox(msg='Changelog:', markdown_msg=msg)

    def init_sentry(self):
        """Add user-related scope information to sentry"""
        with configure_scope() as scope:  # type: ignore
            scope.user = dict(username=self.username,
                              email=self.get_setting('email'))
            # scope.set_extra('version', VERSION) # added to sentry release field

    def active_table_widget(self) -> tbls.TableWidget:
        """Current active TableWidget"""
        return self.tabs.currentWidget()

    @property
    def t(self) -> tbls.TableWidget:
        """Convenience property wrapper for active TableWidget"""
        return self.active_table_widget()

    def active_table(self) -> Union[tbls.TableView, None]:
        """Current active TableView"""
        table_widget = self.active_table_widget()
        if not table_widget is None:
            return table_widget.view

    @property
    def tv(self) -> Union[tbls.TableView, None]:
        """Convenience property wrapper for active TableView"""
        return self.active_table()

    def show_changeminesite(self):
        dlg = dlgs.ChangeMinesite(parent=self)
        return dlg.exec()

    @er.errlog('Close event failed.')
    def closeEvent(self, event):
        s = self.settings
        s.setValue('window size', self.size())
        s.setValue('window position', self.pos())
        s.setValue('screen', self.geometry().center())
        s.setValue('minesite', self.minesite)
        s.setValue('active table', self.active_table_widget().title)

        # save current TableView column state
        self.tv.save_header_state()

        # update on closeEvent if update available... maybe not yet
        # if self.updater.update_available:
        #     self._install_update(updater=self.updater, ask_user=False)

    def get_setting(self, key: str, default: Any = None) -> Any:
        """Convenience accessor to global settings"""
        return gbl.get_setting(key=key, default=default)

    def get_username(self):
        s = self.settings
        username = self.get_setting('username')
        email = self.get_setting('email')

        if username is None or email is None:
            self.set_username()
            username = self.username

        return username

    def set_username(self):
        # show username dialog and save first/last name to settings
        s = self.settings
        dlg = dlgs.InputUserName(self)
        if not dlg.exec():
            return

        s.setValue('username', dlg.username)
        s.setValue('email', dlg.email)
        self.username = dlg.username

        if hasattr(self, 'u'):
            self.u.username = dlg.username
            self.u.email = dlg.email

    @property
    def driver(self) -> Union[WebDriver, None]:
        """Save global Chrome WebDriver to reuse etc for TSI or SAP"""
        return self._driver if hasattr(self, '_driver') else None

    @driver.setter
    def driver(self, driver: WebDriver):
        self._driver = driver

    def open_sap(self):
        from smseventlog.utils.web import SuncorWorkRemote
        self.sc = SuncorWorkRemote(mw=self, _driver=self.driver)

        Worker(func=self.sc.open_sap, mw=self) \
            .add_signals(signals=('result', dict(func=self.handle_sap_result))) \
            .start()
        self.update_statusbar('Opening SAP...')

    def handle_sap_result(self, sc=None):
        """just need to keep a referece to the driver in main thread so chrome doesnt close"""
        if sc is None:
            log.warning('SAP not opened properly')
            return

        self.driver = sc.driver
        self.update_statusbar('SAP started.', success=True)

    def get_menu(self, name: Union[str, 'QMenu']) -> 'QMenu':
        """Get QMenu if exists or create

        Returns
        -------
        QMenu
            menu bar
        """
        if isinstance(name, str):
            menu = self.menus.get(name, None)

            if menu is None:
                bar = self.menuBar()
                menu = bar.addMenu(name.title())
                self.menus[name] = menu
        else:
            menu = name

        return menu

    def add_action(self,
                   name: str,
                   func: Callable,
                   menu: str = None,
                   shortcut: str = None,
                   tooltip: str = None,
                   label_text: str = None,
                   parent: QWidget = None,
                   **kw) -> QAction:
        """Convenience func to create QAction and add to menu bar

        Parameters
        ----------
        name : str
            Action name

        Returns
        -------
        QAction
        """
        name_action = name.replace(' ', '_').lower()
        name_key = f'act_{name_action}'
        name = f.nice_title(name.replace(
            '_', ' ')) if label_text is None else label_text

        if parent is None:
            parent = self

        act = QAction(name, parent, triggered=func, **kw)

        if not shortcut is None:
            act.setShortcut(QKeySequence(shortcut))

        act.setToolTip(tooltip)
        # act.setShortcutContext(Qt.ShortcutContext.WidgetShortcut)
        act.setShortcutVisibleInContextMenu(True)

        setattr(parent, name_key, act)

        if not menu is None:
            menu = self.get_menu(menu)

            menu.addAction(act)
        else:
            parent.addAction(act)

        return act

    def add_actions(self,
                    actions: dict,
                    menu: Union[str, 'QMenu'] = None) -> None:
        """Add dict of multiple actions to menu bar

        Parameters
        ----------
        actions : dict
            dict of menu_name: {action: func|kw}
        """
        menu = self.get_menu(menu)

        for name, kw in actions.items():
            if not isinstance(kw, dict):
                kw = dict(func=kw)

            if 'submenu' in name:
                # create submenu, recurse
                submenu = menu.addMenu(name.replace('submenu_', '').title())
                self.add_actions(menu=submenu, actions=kw)

            else:
                if 'sep' in kw:
                    kw.pop('sep')
                    menu.addSeparator()

                self.add_action(name=name, menu=menu, **kw)

    def create_actions(self) -> None:
        """Initialize menubar actions"""
        t, tv = self.active_table_widget, self.active_table

        menu_actions = dict(
            file=dict(
                add_new_row=dict(func=lambda: t().show_addrow(),
                                 shortcut='Ctrl+Shift+N'),
                refresh_menu=dict(sep=True,
                                  func=lambda: t().show_refresh(),
                                  shortcut='Ctrl+R'),
                refresh_all_open=dict(
                    func=lambda: t().refresh_allopen(default=True),
                    shortcut='Ctrl+Shift+R'),
                reload_last_query=dict(
                    func=lambda: t().refresh(last_query=True),
                    shortcut='Ctrl+Shift+L'),
                previous_tab=dict(sep=True,
                                  func=lambda: self.tabs.activate_previous(),
                                  shortcut='Meta+Tab'),
                change_minesite=dict(func=self.show_changeminesite,
                                     shortcut='Ctrl+Shift+M'),
                view_folder=dict(func=lambda: t().view_folder(),
                                 shortcut='Ctrl+Shift+V'),
                submenu_reports=dict(
                    fleet_monthly_report=lambda: self.create_monthly_report(
                        'Fleet Monthly'),
                    FC_report=lambda: self.create_monthly_report('FC'),
                    SMR_report=lambda: self.create_monthly_report('SMR'),
                    PLM_report=dict(sep=True, func=self.create_plm_report),
                    import_PLM_manual=self.import_plm_manual),
                import_downloads=dict(sep=True, func=self.import_downloads),
                preferences=dict(sep=True,
                                 func=self.show_preferences,
                                 shortcut='Ctrl+,')),
            edit=dict(
                find=dict(func=lambda: tv().show_search(), shortcut='Ctrl+F')),
            table=dict(
                email_table=lambda: t().email_table(),
                email_table_selection=lambda: t().email_table(selection=True),
                export_table_excel=lambda: t().export_df('xlsx'),
                export_table_CSV=lambda: t().export_df('csv'),
                toggle_color=dict(sep=True,
                                  func=lambda: tv().data_model.toggle_color()),
                jump_first_last_row=dict(func=lambda: tv().jump_top_bottom(),
                                         shortcut='Ctrl+Shift+J'),
                reset_column_layout=dict(
                    func=lambda: tv().reset_header_state())),
            rows=dict(open_tsi=dict(func=lambda: t().open_tsi(),
                                    label_text='Open TSI'),
                      delete_row=lambda: t().remove_row(),
                      update_component=lambda: t().show_component(),
                      details_view=dict(func=lambda: t().show_details(),
                                        shortcut='Ctrl+Shift+D')),
            database=dict(
                update_component_SMR=update_comp_smr,
                update_FC_status_clipboard=lambda: fc.
                update_scheduled_sap(exclude=dlgs.inputbox(
                    msg=
                    '1. Enter FCs to exclude\n2. Copy FC Data from SAP to clipboard\n\nExclude:',
                    title='Update Scheduled FCs SAP'),
                                     table_widget=t()),
                reset_database_connection=dict(sep=True, func=db.reset),
                reset_database_tables=db.clear_saved_tables,
                open_SAP=dict(sep=True, func=self.open_sap)),
            help=dict(about=dlgs.about,
                      check_for_update=self.check_update,
                      show_changelog=self.show_full_changelog,
                      email_error_logs=self.email_err_logs,
                      open_documentation=lambda: f.open_url(cf.config['url'][
                          'docs']),
                      submit_issue=dict(
                          func=lambda: f.open_url(cf.config['url']['issues']),
                          label_text='Submit issue or Feature Request'),
                      reset_username=dict(sep=True, func=self.set_username),
                      test_error=self.test_error))

        # reset credentials prompts
        for c in ('TSI', 'SMS', 'exchange', 'SAP'):
            menu_actions['help'][
                f'reset_{c}_credentials'] = lambda x, c=c: CredentialManager(
                    c).prompt_credentials()

        for menu, m_act in menu_actions.items():
            self.add_actions(actions=m_act, menu=menu)

        # other actions which don't go in menubar
        other_actions = dict(
            refresh_last_week=lambda: t().refresh_lastweek(base=True),
            refresh_last_month=lambda: t().refresh_lastmonth(base=True),
            update_SMR=dict(
                func=lambda: t().update_smr(),
                tooltip='Update selected event with SMR from database.'),
            show_SMR_history=lambda: t().show_smr_history())

        self.add_actions(actions=other_actions)

    def test_error(self) -> None:
        """Just raise test error"""
        raise RuntimeError('This is a test error.')

    def contextMenuEvent(self, event):
        """Add actions to right click menu, dependent on currently active table
        """
        child = self.childAt(event.pos())

        menu = QMenu(self)
        # menu.setToolTipsVisible(True)

        table_widget = self.active_table_widget()
        for section in table_widget.context_actions.values():
            for action in section:
                name_action = f'act_{action}'
                try:
                    menu.addAction(getattr(self, name_action))
                except Exception as e:
                    try:
                        menu.addAction(getattr(table_widget, name_action))
                    except Exception as e:
                        log.warning(
                            f'Couldn\'t add action to context menu: {action}')

            menu.addSeparator()

        action = menu.exec(self.mapToGlobal(event.pos()))

    def create_monthly_report(self, name: str):
        """Create report in worker thread from dialog menu

        Parameters
        ----------
        name : str
            ['Fleet Monthly', 'FC']
        """

        dlg = dlgs.BaseReportDialog(window_title=f'{name} Report')
        if not dlg.exec():
            return

        from smseventlog.reports import FCReport, FleetMonthlyReport
        from smseventlog.reports import Report as _Report
        from smseventlog.reports import SMRReport
        Report = {
            'Fleet Monthly': FleetMonthlyReport,
            'FC': FCReport,
            'SMR': SMRReport
        }[name]  # type: _Report

        rep = Report(d=dlg.d, minesite=dlg.items['MineSite'])  # type: ignore

        Worker(func=rep.create_pdf, mw=self) \
            .add_signals(signals=('result', dict(func=self.handle_monthly_report_result))) \
            .start()

        self.update_statusbar('Creating Fleet Monthly Report...')

    def handle_monthly_report_result(self, rep=None):
        if rep is None:
            return
        rep.open_()

        msg = f'Report:\n\n"{rep.title}"\n\nsuccessfully created. Email now?'
        if dlgs.msgbox(msg=msg, yesno=True):
            rep.email()

    def import_plm_manual(self):
        """Allow user to manually select haulcycle files to upload"""
        t = self.active_table_widget()
        e = t.e
        if not e is None:
            from smseventlog import eventfolders as efl
            unit, dateadded = e.Unit, e.DateAdded
            uf = efl.UnitFolder(unit=unit)
            p = uf.p_unit
        else:
            # No unit selected, try to get minesite equip path
            p = cf.p_drive / cf.config['EquipPaths'].get(
                self.minesite.replace('-', ''), '')

        if p is None:
            p = Path.home() / 'Desktop'

        lst_csv = dlgs.select_multi_files(p_start=p)
        if not lst_csv:
            return  # user didn't select anything

        from smseventlog.data.internal import utils as utl
        Worker(func=utl.combine_import_csvs, mw=self, lst_csv=lst_csv, ftype='plm') \
            .add_signals(('result', dict(func=self.handle_import_result_manual))) \
            .start()

        self.update_statusbar(
            'Importing haul cylce files from network drive (this may take a few minutes)...'
        )

    def create_plm_report(self):
        """Trigger plm report from current unit selected in table"""
        from smseventlog.data.internal import plm

        view = self.active_table()
        try:
            e = view.e
            unit, d_upper = e.Unit, e.DateAdded
        except er.NoRowSelectedError:
            # don't set dialog w unit and date, just default
            unit, d_upper, e = None, None, None

        # Report dialog will always set final unit etc
        dlg = dlgs.PLMReport(unit=unit, d_upper=d_upper)
        ok = dlg.exec()
        if not ok:
            return  # user exited

        m = dlg.get_items(lower=True)  # unit, d_upper, d_lower

        # check if unit selected matches event selected
        if not e is None:
            if not e.Unit == m['unit']:
                e = None

        m['e'] = e
        # NOTE could make a func 'rename_dict_keys'
        m['d_upper'], m['d_lower'] = m['date upper'], m['date lower']

        # check max date in db
        maxdate = plm.max_date_plm(unit=m['unit'])

        if maxdate + delta(days=5) < m['d_upper']:
            # worker will call back and make report when finished
            if not fl.drive_exists(warn=False):
                msg = 'Can\'t connect to P Drive. Create report without updating records first?'
                if dlgs.msgbox(msg=msg, yesno=True):
                    self.make_plm_report(**m)

                return

            Worker(func=plm.update_plm_single_unit, mw=self, unit=m['unit']) \
                .add_signals(
                    signals=('result', dict(
                        func=self.handle_import_result,
                        kw=m))) \
                .start()

            msg = f'Max date in db: {maxdate:%Y-%m-%d}. ' \
                + 'Importing haul cylce files from network drive, this may take a few minutes...'
            self.update_statusbar(msg=msg)

        else:
            # just make report now
            self.make_plm_report(**m)

    def handle_import_result_manual(self, rowsadded=None, **kw):
        if not rowsadded is None:
            msg = dict(msg=f'PLM records added to database: {rowsadded}',
                       success=rowsadded > 0)
        else:
            msg = 'Warning: Failed to import PLM records.'

        self.update_statusbar(msg)

    def handle_import_result(self, m_results=None, **kw):
        if m_results is None:
            return

        rowsadded = m_results['rowsadded']
        self.update_statusbar(f'PLM records added to database: {rowsadded}',
                              success=True)

        self.make_plm_report(**kw)

    def make_plm_report(self, e=None, **kw):
        """Actually make the report pdf"""
        from smseventlog import eventfolders as efl
        from smseventlog.reports import PLMUnitReport
        rep = PLMUnitReport(mw=self, **kw)

        if not e is None:
            ef = efl.EventFolder.from_model(e)
            p = ef._p_event
        else:
            ef = None

        # If cant get event folder, ask to create at desktop
        if ef is None or not ef.check(check_pics=False, warn=False):
            p = Path.home() / 'Desktop'
            msg = 'Can\'t get event folder, create report at desktop?'
            if not dlgs.msgbox(msg=msg, yesno=True):
                return

        Worker(func=rep.create_pdf, mw=self, p_base=p) \
            .add_signals(signals=('result', dict(func=self.handle_plm_result, kw=kw))) \
            .start()

        self.update_statusbar(f'Creating PLM report for unit {kw["unit"]}...')

    def handle_plm_result(self, rep=None, unit=None, **kw):
        if rep is False:
            # not super robust, but just warn if no rows in query
            msg = 'No rows returned in query, can\'t create report!'
            dlgs.msg_simple(msg=msg, icon='warning')

        if not rep or not rep.p_rep.exists():
            self.update_statusbar('Failed to create PLM report.', warn=True)
            return

        self.update_statusbar(f'PLM report created for unit {unit}',
                              success=True)

        msg = f'Report:\n\n"{rep.title}"\n\nsuccessfully created. Open now?'
        if dlgs.msgbox(msg=msg, yesno=True):
            rep.open_()

    def email_err_logs(self):
        """Collect and email error logs to simplify for user"""
        docs = []

        def _collect_logs(p):
            return [p for p in p.glob('*log*')] if p.exists() else []

        # collect sms logs
        p_sms = cf.p_applocal / 'logging'
        docs.extend(_collect_logs(p_sms))

        # collect pyupdater logs
        i = 1 if cf.is_win else 0
        p_pyu = cf.p_applocal.parents[1] / 'Digital Sapphire/PyUpdater/logs'
        docs.extend(_collect_logs(p_pyu))

        from smseventlog.utils import email as em

        subject = f'Error Logs - {self.username}'
        body = 'Thanks Jayme,<br><br>I know you\'re trying your best. \
            The Event Log is amazing and we appreciate all your hard work!'

        msg = em.Message(subject=subject,
                         body=body,
                         to_recip=['*****@*****.**'],
                         show_=False)
        msg.add_attachments(docs)
        msg.show()

    def import_downloads(self) -> None:
        """Select and import dls files to p-drive"""
        if not fl.drive_exists():
            return

        from smseventlog.data.internal import dls

        # get dls filepath
        lst_dls = dlgs.select_multi_folders(p_start=cf.desktop)
        if lst_dls is None:
            msg = 'User failed to select downloads folders.'
            self.update_statusbar(msg=msg, warn=True)
            return

        # start uploads for each dls folder selected
        for p_dls in lst_dls:
            Worker(func=dls.import_dls, mw=self, p=p_dls) \
                .add_signals(signals=('result', dict(func=self.handle_dls_result))) \
                .start()

        self.update_statusbar(msg='Started downloads upload in worker thread.')

    def handle_dls_result(self, result: dict = None, **kw):
        if isinstance(result, dict):
            name, time_total = '', ''
            try:
                name = result.pop('name')
                time_total = f.mins_secs(result.pop('time_total'))

                # join remaining processed files/times
                msg_result = ', '.join([
                    f'{k}: ({m["num"]}, {f.mins_secs(m["time"])})'
                    for k, m in result.items()
                ])
            except:
                msg_result = ''
                log.warning('Failed to build upload string')

            msg = f'Successfully uploaded downloads folder "{name}", ({time_total}). \
            Files processed/rows imported: {msg_result}'

            msg = dict(msg=msg, success=True)

        else:
            msg = dict(msg='Failed to upload downloads.', warn=True)

        self.update_statusbar(msg=msg)

    def show_preferences(self) -> None:
        """Show preferences dialog to allow user to change global settings"""
        dlg = dlgs.Preferences(parent=self)
        dlg.exec()