Ejemplo n.º 1
0
    def _InitDock(self):
        # self.setDockNestingEnabled(True)
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)

        topDock = QDockWidget("菜单", self)
        topDock.setSizePolicy(sizePolicy)
        topDock.setObjectName("topDock")
        topDock.setWidget(self.m_MenuWidget)

        bottomDock = QDockWidget("搜索", self)
        bottomDock.setSizePolicy(sizePolicy)
        bottomDock.setObjectName("bottomDock")
        bottomDock.setWidget(self.m_SearchWidget)

        leftDock = QDockWidget("属性", self)
        leftDock.setSizePolicy(sizePolicy)
        leftDock.setObjectName("leftDock")
        leftDock.setWidget(self.m_BPAttrWidget)

        rightDock = QDockWidget("细节", self)
        rightDock.setSizePolicy(sizePolicy)
        rightDock.setObjectName("rightDock")
        rightDock.setWidget(self.m_DeltailWidget)

        logDock = QDockWidget("日志面板", self)
        logDock.setSizePolicy(sizePolicy)
        logDock.setObjectName("logDock")
        logDock.setWidget(self.m_LogWidget)

        self.addDockWidget(Qt.RightDockWidgetArea, rightDock)
        self.addDockWidget(Qt.TopDockWidgetArea, topDock)
        self.addDockWidget(Qt.BottomDockWidgetArea, bottomDock)
        self.addDockWidget(Qt.BottomDockWidgetArea, logDock)
        self.tabifyDockWidget(bottomDock, logDock)
        logDock.raise_()

        self.addDockWidget(Qt.LeftDockWidgetArea, leftDock)
        self.setCentralWidget(self.m_BPTabWidget)
Ejemplo n.º 2
0
    def InitDock(self):
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)

        topDock = QDockWidget("顶侧面板", self)
        topDock.setSizePolicy(sizePolicy)
        topDock.setObjectName("topDock")
        topDock.setWidget(CMyWidget("顶侧面板"))

        bottomDock = QDockWidget("底侧面板", self)
        bottomDock.setSizePolicy(sizePolicy)
        bottomDock.setObjectName("bottomDock")
        bottomDock.setWidget(CMyWidget("底侧面板"))

        bottomDock2 = QDockWidget("底侧面板2", self)
        bottomDock2.setSizePolicy(sizePolicy)
        bottomDock2.setObjectName("bottomDock2")
        bottomDock2.setWidget(CMyWidget("底侧面板2"))

        leftDock = QDockWidget("左侧面板", self)
        leftDock.setSizePolicy(sizePolicy)
        leftDock.setObjectName("leftDock")
        leftDock.setWidget(CMyWidget("左侧面板"))

        rightDock = QDockWidget("右侧面板", self)
        rightDock.setSizePolicy(sizePolicy)
        rightDock.setObjectName("rightDock")
        rightDock.setWidget(CMyWidget("右侧面板"))

        self.addDockWidget(Qt.RightDockWidgetArea, rightDock)
        self.addDockWidget(Qt.TopDockWidgetArea, topDock)

        self.addDockWidget(Qt.BottomDockWidgetArea, bottomDock)
        self.addDockWidget(Qt.BottomDockWidgetArea, bottomDock2)
        self.tabifyDockWidget(bottomDock, bottomDock2)
        bottomDock2.raise_()

        self.addDockWidget(Qt.LeftDockWidgetArea, leftDock)
        self.setCentralWidget(CMyWidget("中心控件"))
 def create_table_dock(self):
     
     self.create_open_trades()
     open_trades_dock = QDockWidget("Open Trades", self)
     open_trades_dock.setWidget(self.open_trades)
     self.addDockWidget(Qt.BottomDockWidgetArea, open_trades_dock)
     # self.viewMenu.addAction(dock.toggleViewAction())
     
     self.create_working_trades()
     working_trades_dock = QDockWidget("Working Trades", self)
     working_trades_dock.setWidget(self.working_trades)
     self.addDockWidget(Qt.BottomDockWidgetArea, working_trades_dock)
     # self.viewMenu.addAction(dock.toggleViewAction())
     
     self.create_trade_history()
     trade_history_dock = QDockWidget("Trade History", self)
     trade_history_dock.setWidget(self.trade_history)
     self.addDockWidget(Qt.BottomDockWidgetArea, trade_history_dock)
     # self.viewMenu.addAction(dock.toggleViewAction())
     
     self.tabifyDockWidget(open_trades_dock, working_trades_dock)
     self.tabifyDockWidget(working_trades_dock, trade_history_dock)
     open_trades_dock.raise_()
Ejemplo n.º 4
0
    def initUI(self):
        sender_widget = sender()
        capture_widget = capture()
        DockWidget_Sender = QDockWidget('Sender', self)
        DockWidget_Sender.setWidget(sender_widget)
        DockWidget_Capture = QDockWidget('Capture', self)
        DockWidget_Capture.setWidget(capture_widget)

        DockWidget_Sender.setFeatures(QDockWidget.NoDockWidgetFeatures)
        DockWidget_Sender.setAllowedAreas(Qt.TopDockWidgetArea)
        self.addDockWidget(Qt.TopDockWidgetArea, DockWidget_Sender)

        DockWidget_Capture.setFeatures(QDockWidget.DockWidgetFloatable |
                                       QDockWidget.DockWidgetMovable)
        DockWidget_Capture.setAllowedAreas(Qt.TopDockWidgetArea)
        self.addDockWidget(Qt.TopDockWidgetArea, DockWidget_Capture)

        self.tabifyDockWidget(DockWidget_Sender, DockWidget_Capture)
        DockWidget_Sender.raise_()

        statusbar = self.statusBar()
        menubar = self.menuBar()

        filemenu = menubar.addMenu('&File')
        import_act = QAction('Import', self)
        export_act = QAction('Export', self)
        exit_act = QAction('Exit', self)
        filemenu.addAction(import_act)
        filemenu.addAction(export_act)
        filemenu.addSeparator()
        filemenu.addAction(exit_act)

        aboutmenu = menubar.addMenu('&About')
        about_act = QAction('About', self)
        aboutmenu.addAction(about_act)

        self.setWindowTitle('Packet Sender')
Ejemplo n.º 5
0
class QDebugPanel(QMainWindow):
    def __init__(self, app, flags=None):
        super(QDebugPanel, self).__init__(flags)
        self.setDockOptions(QMainWindow.AnimatedDocks
                            | QMainWindow.AllowNestedDocks
                            | QMainWindow.AllowTabbedDocks)

        self.app = app
        self.q_settings = app.q_settings

        screen_size = QtWidgets.QDesktopWidget().screenGeometry(-1)
        m_width = screen_size.width()

        self.memory_panel = HexEditor(self.app)
        self.memory_panel.debug_panel = self
        self.memory_panel.dataChanged.connect(self.on_memory_modified)

        self.disassembly_panel = DisassemblyView(self.app)
        self.disassembly_panel.debug_panel = self

        self.dock_memory_panel = QDockWidget('Memory', self)
        self.dock_memory_panel.setWidget(self.memory_panel)
        self.dock_memory_panel.setObjectName('memory')

        self.dock_disassembly_panel = QDockWidget('Disassembly', self)
        self.dock_disassembly_panel.setWidget(self.disassembly_panel)
        self.dock_disassembly_panel.setObjectName('disassembly')

        self.addDockWidget(Qt.LeftDockWidgetArea, self.dock_memory_panel,
                           Qt.Horizontal)
        self.addDockWidget(Qt.LeftDockWidgetArea, self.dock_disassembly_panel,
                           Qt.Horizontal)
        if m_width >= 1920:
            self.splitDockWidget(self.dock_memory_panel,
                                 self.dock_disassembly_panel, Qt.Horizontal)
        else:
            self.tabifyDockWidget(self.dock_memory_panel,
                                  self.dock_disassembly_panel)

        self.restoreUiState()

    def restoreUiState(self):
        ui_state = self.q_settings.value('dwarf_debug_ui_state')
        if ui_state:
            self.restoreGeometry(ui_state)
        window_state = self.q_settings.value('dwarf_debug_ui_window')
        if window_state:
            self.restoreState(window_state)

    def closeEvent(self, event):
        self.q_settings.setValue('dwarf_debug_ui_state', self.saveGeometry())
        self.q_settings.setValue('dwarf_debug_ui_window', self.saveState())

    def showEvent(self, event):
        main_width = self.size().width()
        new_widths = [int(main_width * .4), int(main_width * .5)]
        self.resizeDocks([self.dock_memory_panel, self.dock_disassembly_panel],
                         new_widths, Qt.Horizontal)
        return super().showEvent(event)

    def on_context_setup(self):
        self.memory_panel.on_context_setup()

    def on_memory_modified(self, pos, length):
        data_pos = self.memory_panel.base + pos
        data = self.memory_panel.data[pos:pos + length]
        data = [data[0]]  # todo: strange js part

        if self.app.dwarf.dwarf_api('writeBytes', [data_pos, data]):
            pass
        else:
            utils.show_message_box('Failed to write Memory')

    def raise_memory_panel(self):
        self.dock_memory_panel.raise_()

    def raise_disassembly_panel(self):
        self.dock_disassembly_panel.raise_()

    def jump_to_address(self, address, view=DEBUG_VIEW_MEMORY, force=False):
        address = utils.parse_ptr(address)

        if not force:
            if view == DEBUG_VIEW_MEMORY:
                if self.memory_panel.number_of_lines() > 0:
                    if self.is_address_in_view(view, address):
                        return
            elif view == DEBUG_VIEW_DISASSEMBLY:
                if self.disassembly_panel.number_of_lines() > 0:
                    if self.is_address_in_view(view, address):
                        return

        self.app.show_progress('reading data...')
        self.app.dwarf.read_range_async(
            address, lambda base, data, offset: self._apply_data(
                base, data, offset, view=view))

    def _apply_data(self, base, data, offset, view=DEBUG_VIEW_MEMORY):
        self.app.hide_progress()

        if view == DEBUG_VIEW_MEMORY:
            self.memory_panel.set_data(data, base=base, offset=offset)
            if not self.dock_memory_panel.isVisible():
                self.dock_memory_panel.show()
            self.raise_memory_panel()

            if self.disassembly_panel.number_of_lines() == 0:
                self.disassembly_panel.disasm(base, data, offset)
        elif view == DEBUG_VIEW_DISASSEMBLY:
            self.disassembly_panel.disasm(base, data, offset)
            if not self.dock_disassembly_panel.isVisible():
                self.dock_disassembly_panel.show()
            self.raise_disassembly_panel()

            if self.memory_panel.number_of_lines() == 0:
                self.memory_panel.set_data(data, base=base, offset=offset)

    def is_address_in_view(self, view, address):
        if view == DEBUG_VIEW_MEMORY:
            if self.memory_panel.data:
                ptr_exists = self.memory_panel.base <= address <= self.memory_panel.base + len(
                    self.memory_panel.data)
                if ptr_exists:
                    self.memory_panel.caret.position = address - self.memory_panel.base
                    return True
        elif view == DEBUG_VIEW_DISASSEMBLY:
            if self.disassembly_panel.visible_lines() > 0:
                line_index_for_address = self.disassembly_panel.get_line_for_address(
                    address)
                if line_index_for_address >= 0:
                    self.disassembly_panel.highlighted_line = line_index_for_address
                    self.disassembly_panel.verticalScrollBar().setValue(
                        line_index_for_address)
                    return True
        return False

    def on_cm_jump_to_address(self, view=DEBUG_VIEW_MEMORY):
        ptr, _ = InputDialog.input_pointer(self.app)
        if ptr > 0:
            self.jump_to_address(ptr, view=view)

    def dump_data(self, address, _len):
        def _dump(ptr, data):
            if data is not None:
                from PyQt5.QtWidgets import QFileDialog
                _file = QFileDialog.getSaveFileName(self.app)
                with open(_file[0], 'wb') as f:
                    f.write(data)

        self.app.dwarf.read_memory_async(address, _len, _dump)
Ejemplo n.º 6
0
class MainWindow(QMainWindow):
    def __init__(self, context: 'core.context.Context', input: str = None):
        super().__init__()
        self._context = context
        self._logger = context.logger()

        #############################################
        #   Initialize status bar
        #############################################

        self._message_widget = self._init_message_widget()
        self._logger.handlers[0].addFilter(
            self._init_log_filter(self._message_widget))
        self.statusBar().addWidget(self._message_widget)
        self.statusBar().addPermanentWidget(self._init_hidden_dialog())

        #############################################
        #   Initialize docks
        #############################################

        # Initialize empty dock
        self._empty_dock = QDockWidget("", self)
        self._empty_dock.setFeatures(QDockWidget.NoDockWidgetFeatures)
        self.addDockWidget(Qt.BottomDockWidgetArea, self._empty_dock)
        self._empty_dock.visibilityChanged.connect(self.show_dock)

        # Initialize log dock
        self._log_entries = []
        self._log_dock = LogDock(self._log_entries, self)
        self._log_dock.clearEvent.connect(self._message_widget.resetCount)
        self._log_dock.setFeatures(QDockWidget.DockWidgetFloatable)
        self.addDockWidget(Qt.BottomDockWidgetArea, self._log_dock)
        self.tabifyDockWidget(self._empty_dock, self._log_dock)
        self._log_dock.visibilityChanged.connect(self.show_dock)

        # Initialize hex dock
        self._hex_dock = HexDock(self._context, self)
        self._hex_dock.setFeatures(QDockWidget.DockWidgetFloatable)
        self.addDockWidget(Qt.BottomDockWidgetArea, self._hex_dock)
        self.tabifyDockWidget(self._empty_dock, self._hex_dock)
        self._hex_dock.visibilityChanged.connect(self.show_dock)

        # Initialize dock tab bar
        self._dock_tab_bar = self.findChild(QTabBar, "")
        self._dock_tab_bar.tabBarDoubleClicked.connect(
            lambda e: self._empty_dock.raise_())

        # Do only dock tab bar on startup
        self._empty_dock.raise_()

        #############################################
        #   Initialize window
        #############################################

        self._init_window_size()
        self.setWindowTitle("Decoder++")
        self.setWindowIcon(
            QIcon(
                os.path.join(self._context.getAppPath(), 'images',
                             'dpp_128.png')))

    def _init_log_filter(self, message_widget):
        """ Initializes the log filter which catches log events to be shown in the statusbar. """
        log_filter = LogFilter(self)
        log_filter.logInfoEvent.connect(message_widget.showInfo)
        log_filter.logErrorEvent.connect(message_widget.showError)
        log_filter.logDebugEvent.connect(message_widget.showDebug)
        return log_filter

    def _init_window_size(self):
        """ Initializes the window size. Looks and uses any previously saved sizing. """
        size = self._context.config.getSize()
        if size:
            self.resize(self._context.config.getSize())
        self.setMinimumWidth(520)
        self.setMinimumHeight(300)

    def _init_message_widget(self) -> MessageWidget:
        """ Inits the message widget located in the statusbar. """
        message_widget = MessageWidget()
        message_widget.messageReceived.connect(self._log_message)
        message_widget.infoClicked.connect(
            lambda: self._toggle_log_dock(LogDock.Filter.INFO))
        message_widget.errorClicked.connect(
            lambda: self._toggle_log_dock(LogDock.Filter.ERROR))
        message_widget.messageClicked.connect(lambda: self._toggle_log_dock(
            LogDock.Filter.INFO, LogDock.Filter.ERROR))
        return message_widget

    def _init_hidden_dialog(self):
        """ Inits the icon which opens the hidden dialog on mouse press. """
        about_label = IconLabel(
            self,
            QIcon(
                os.path.join(self._context.getAppPath(), 'images',
                             'hidden.png')))
        about_label.mousePressEvent = lambda e: self._show_hidden_dialog()
        return about_label

    def _log_message(self, type: str, message: str):
        """
        Adds the message to the log dock.
        :param type: the type of the message (e.g. info, error, debug).
        :param message: the message.
        """
        now = datetime.datetime.now().time()
        log_entry = LogEntry(
            "{hour:02d}:{minute:02d}:{second:02d}".format(hour=now.hour,
                                                          minute=now.minute,
                                                          second=now.second),
            type, message)
        if self._log_dock:
            self._log_dock.addItem(log_entry)
        self._log_entries.append(log_entry)

    def _show_hidden_dialog(self):
        """ Shows the hidden dialog. """
        hidden_dialog = ConfigDialog(self, self._context)
        hidden_dialog.exec_()

    def _toggle_log_dock(self, *filters: List[str], **kwargs):
        """ Shows/Hides the log dock. """
        is_log_dock_visible = not self._log_dock.visibleRegion().isEmpty()
        if not is_log_dock_visible:
            self._log_dock.raise_()
            for filter in self._log_dock.getFilters():
                self._log_dock.setFilterChecked(filter, filter in filters)
        else:
            self._empty_dock.raise_()

    def _toggle_hex_dock(self):
        """ Shows/Hides the hex dock. """
        is_hex_dock_visible = not self._hex_dock.visibleRegion().isEmpty()
        if not is_hex_dock_visible:
            self._hex_dock.raise_()
        else:
            self._empty_dock.raise_()

    def show_dock(self, e: QEvent):
        """ Shows/Hides the docks. """
        is_empty_dock_visible = not self._empty_dock.visibleRegion().isEmpty()
        for dock in [self._log_dock, self._hex_dock]:
            if is_empty_dock_visible:
                if not dock.isFloating():
                    dock.hide()
            else:
                dock.show()

    def closeEvent(self, e: QEvent):
        """ Closes the main window and saves window-size and -position. """
        self._context.config.setSize(self.size())
        self._context.config.setPosition(self.pos())
        e.accept()

    def setWindowIcon(self, icon: QIcon):
        """ Sets the window icon of the main window. """
        super(__class__, self).setWindowIcon(icon)
        if os.name == 'nt':
            # Set explicit app id to show decoder-plus-plus icon in the taskbar.
            ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
                self._context.getAppID())
Ejemplo n.º 7
0
class QDebugPanel(QMainWindow):
    def __init__(self, app, flags=None):
        super(QDebugPanel, self).__init__(flags)
        self.setDockOptions(QMainWindow.AnimatedDocks | QMainWindow.AllowNestedDocks)

        self.app = app
        self.q_settings = app.q_settings

        self.functions_list = DwarfListView()
        self.functions_list_model = QStandardItemModel(0, 1)
        self.functions_list_model.setHeaderData(0, Qt.Horizontal, '')
        self.functions_list.setModel(self.functions_list_model)
        self.functions_list.setHeaderHidden(True)
        self.functions_list.doubleClicked.connect(self._function_double_clicked)

        self.dock_functions_list = QDockWidget('Functions', self)
        self.dock_functions_list.setObjectName('functions')
        self.dock_functions_list.setWidget(self.functions_list)
        self.addDockWidget(Qt.LeftDockWidgetArea, self.dock_functions_list)
        self.app.debug_view_menu.addAction(self.dock_functions_list.toggleViewAction())

        self.memory_panel_range = None
        self.disassembly_panel_range = None

        screen_size = QtWidgets.QDesktopWidget().screenGeometry(-1)
        m_width = screen_size.width()

        self.memory_panel = HexEditor(self.app)
        self.memory_panel.debug_panel = self
        self.memory_panel.dataChanged.connect(self.on_memory_modified)

        self.disassembly_panel = DisassemblyView(self.app)
        self.disassembly_panel.debug_panel = self

        self.dock_memory_panel = QDockWidget('Memory', self)
        self.dock_memory_panel.setWidget(self.memory_panel)
        self.dock_memory_panel.setObjectName('memory')

        self.dock_disassembly_panel = QDockWidget('Disassembly', self)
        self.dock_disassembly_panel.setWidget(self.disassembly_panel)
        self.dock_disassembly_panel.setObjectName('disassembly')

        self.addDockWidget(Qt.RightDockWidgetArea, self.dock_memory_panel)
        self.addDockWidget(Qt.RightDockWidgetArea, self.dock_disassembly_panel)
        if m_width >= 1920:
            self.splitDockWidget(self.dock_memory_panel, self.dock_disassembly_panel, Qt.Horizontal)
        else:
            self.tabifyDockWidget(self.dock_memory_panel, self.dock_disassembly_panel)

        self.restoreUiState()

    def restoreUiState(self):
        ui_state = self.q_settings.value('dwarf_debug_ui_state')
        if ui_state:
            self.restoreGeometry(ui_state)
        window_state = self.q_settings.value('dwarf_debug_ui_window')
        if window_state:
            self.restoreState(window_state)

    def closeEvent(self, event):
        self.q_settings.setValue('dwarf_debug_ui_state', self.saveGeometry())
        self.q_settings.setValue('dwarf_debug_ui_window', self.saveState())

    def showEvent(self, event):
        main_width = self.size().width()
        new_widths = []
        new_widths.append(main_width * .1)
        new_widths.append(main_width * .4)
        new_widths.append(main_width * .5)
        self.resizeDocks([
            self.dock_functions_list, self.dock_memory_panel, self.dock_disassembly_panel
        ], new_widths, Qt.Horizontal)
        return super().showEvent(event)

    def update_functions(self, functions_list=None):
        if functions_list is None:
            functions_list = {}
        self.functions_list_model.setRowCount(0)
        for module_info_base in self.app.dwarf.database.modules_info:
            module_info = self.app.dwarf.database.modules_info[module_info_base]
            if len(module_info.functions) > 0:
                self.functions_list.show()
                for function in module_info.functions:
                    functions_list[function.name] = function.address

        for function_name in sorted(functions_list.keys()):
            function_addr = functions_list[function_name]
            item = QStandardItem(function_name.replace('.', '_'))
            item.setData(function_addr, Qt.UserRole + 2)
            self.functions_list_model.appendRow([item])

    def _function_double_clicked(self, model_index):
        item = self.functions_list_model.itemFromIndex(model_index)
        address = item.data(Qt.UserRole + 2)
        self.jump_to_address(address, view=DEBUG_VIEW_DISASSEMBLY)

    def on_context_setup(self):
        self.memory_panel.on_context_setup()

    def on_memory_modified(self, pos, length):
        data_pos = self.memory_panel.base + pos
        data = self.memory_panel.data[pos:pos + length]
        data = [data[0]]  # todo: strange js part

        if self.dwarf.dwarf_api('writeBytes', [data_pos, data]):
            pass
        else:
            utils.show_message_box('Failed to write Memory')

    def raise_memory_panel(self):
        self.dock_memory_panel.raise_()

    def raise_disassembly_panel(self):
        self.dock_disassembly_panel.raise_()

    def jump_to_address(self, address, view=DEBUG_VIEW_MEMORY):
        address = utils.parse_ptr(address)

        if view == DEBUG_VIEW_MEMORY:
            if self.memory_panel_range is not None:
                if self.is_address_in_view(view, address):
                    return

            self.memory_panel_range = \
                Range.build_or_get(self.app.dwarf, address, cb=lambda x: self._apply_range(address, view=view))
        elif view == DEBUG_VIEW_DISASSEMBLY:
            if self.disassembly_panel_range is not None:
                if self.is_address_in_view(view, address):
                    return

            self.disassembly_panel_range = \
                Range.build_or_get(self.app.dwarf, address, cb=lambda x: self._apply_range(address, view=view))

    def _apply_range(self, address, view=DEBUG_VIEW_MEMORY):
        self.update_functions()

        if view == DEBUG_VIEW_MEMORY:
            self.memory_panel.set_data(
                self.memory_panel_range.data, base=self.memory_panel_range.base, focus_address=address)
            if not self.dock_memory_panel.isVisible():
                self.dock_memory_panel.show()
            self.raise_memory_panel()

            if self.disassembly_panel_range is None:
                self.disassembly_panel_range = self.memory_panel_range
                self.disassembly_panel.apply_range(self.disassembly_panel_range)
        elif view == DEBUG_VIEW_DISASSEMBLY:
            self.disassembly_panel.apply_range(self.disassembly_panel_range)
            if not self.dock_disassembly_panel.isVisible():
                self.dock_disassembly_panel.show()
            self.raise_disassembly_panel()

            if self.memory_panel_range is None:
                self.memory_panel_range = self.disassembly_panel_range
                self.memory_panel.set_data(
                    self.memory_panel_range.data, base=self.memory_panel_range.base, focus_address=address)

    def is_address_in_view(self, view, address):
        if view == DEBUG_VIEW_MEMORY:
            if self.memory_panel_range is not None:
                ptr_exists = self.memory_panel.base <= address <= self.memory_panel.base + len(self.memory_panel.data)
                if ptr_exists:
                    self.memory_panel.caret.position = address - self.memory_panel.base
                    return True
        elif view == DEBUG_VIEW_DISASSEMBLY:
            if self.disassembly_panel_range is not None:
                line_index_for_address = self.disassembly_panel.get_line_for_address(address)
                if line_index_for_address >= 0:
                    self.disassembly_panel.verticalScrollBar().setValue(line_index_for_address)
                    return True
        return False

    def on_cm_jump_to_address(self, view=DEBUG_VIEW_MEMORY):
        ptr, _ = InputDialog.input_pointer(self.app)
        if ptr > 0:
            self.jump_to_address(ptr, view=view)

    def dump_data(self, address, _len):
        def _dump(dwarf_range):
            if address + _len > dwarf_range.tail:
                self.display_error('length is higher than range size')
            else:
                data = dwarf_range.data[address:address + _len]
                if data is not None:
                    from PyQt5.QtWidgets import QFileDialog
                    _file = QFileDialog.getSaveFileName(self.app)
                    with open(_file[0], 'wb') as f:
                        f.write(data)
        Range.build_or_get(self.app.dwarf, address, cb=_dump)
Ejemplo n.º 8
0
class MusicPlayer(QMainWindow):
    """MusicPlayer houses all of elements that directly interact with the main window."""

    def __init__(self, parent=None):
        """Initialize the QMainWindow widget.

        The window title, window icon, and window size are initialized here as well
        as the following widgets: QMediaPlayer, QMediaPlaylist, QMediaContent, QMenuBar,
        QToolBar, QLabel, QPixmap, QSlider, QDockWidget, QListWidget, QWidget, and
        QVBoxLayout. The connect signals for relavant widgets are also initialized.
        """
        super(MusicPlayer, self).__init__(parent)
        self.setWindowTitle('Mosaic')

        window_icon = utilities.resource_filename('mosaic.images', 'icon.png')
        self.setWindowIcon(QIcon(window_icon))
        self.resize(defaults.Settings().window_size, defaults.Settings().window_size + 63)

        # Initiates Qt objects to be used by MusicPlayer
        self.player = QMediaPlayer()
        self.playlist = QMediaPlaylist()
        self.playlist_location = defaults.Settings().playlist_path
        self.content = QMediaContent()
        self.menu = self.menuBar()
        self.toolbar = QToolBar()
        self.art = QLabel()
        self.pixmap = QPixmap()
        self.slider = QSlider(Qt.Horizontal)
        self.duration_label = QLabel()
        self.playlist_dock = QDockWidget('Playlist', self)
        self.library_dock = QDockWidget('Media Library', self)
        self.playlist_view = QListWidget()
        self.library_view = library.MediaLibraryView()
        self.library_model = library.MediaLibraryModel()
        self.preferences = configuration.PreferencesDialog()
        self.widget = QWidget()
        self.layout = QVBoxLayout(self.widget)
        self.duration = 0
        self.playlist_dock_state = None
        self.library_dock_state = None

        # Sets QWidget() as the central widget of the main window
        self.setCentralWidget(self.widget)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.art.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)

        # Initiates the playlist dock widget and the library dock widget
        self.addDockWidget(defaults.Settings().dock_position, self.playlist_dock)
        self.playlist_dock.setWidget(self.playlist_view)
        self.playlist_dock.setVisible(defaults.Settings().playlist_on_start)
        self.playlist_dock.setFeatures(QDockWidget.DockWidgetClosable)

        self.addDockWidget(defaults.Settings().dock_position, self.library_dock)
        self.library_dock.setWidget(self.library_view)
        self.library_dock.setVisible(defaults.Settings().media_library_on_start)
        self.library_dock.setFeatures(QDockWidget.DockWidgetClosable)
        self.tabifyDockWidget(self.playlist_dock, self.library_dock)

        # Sets the range of the playback slider and sets the playback mode as looping
        self.slider.setRange(0, self.player.duration() / 1000)
        self.playlist.setPlaybackMode(QMediaPlaylist.Sequential)

        # OSX system menu bar causes conflicts with PyQt5 menu bar
        if sys.platform == 'darwin':
            self.menu.setNativeMenuBar(False)

        # Initiates Settings in the defaults module to give access to settings.toml
        defaults.Settings()

        # Signals that connect to other methods when they're called
        self.player.metaDataChanged.connect(self.display_meta_data)
        self.slider.sliderMoved.connect(self.seek)
        self.player.durationChanged.connect(self.song_duration)
        self.player.positionChanged.connect(self.song_position)
        self.player.stateChanged.connect(self.set_state)
        self.playlist_view.itemActivated.connect(self.activate_playlist_item)
        self.library_view.activated.connect(self.open_media_library)
        self.playlist.currentIndexChanged.connect(self.change_index)
        self.playlist.mediaInserted.connect(self.initialize_playlist)
        self.playlist_dock.visibilityChanged.connect(self.dock_visiblity_change)
        self.library_dock.visibilityChanged.connect(self.dock_visiblity_change)
        self.preferences.dialog_media_library.media_library_line.textChanged.connect(self.change_media_library_path)
        self.preferences.dialog_view_options.dropdown_box.currentIndexChanged.connect(self.change_window_size)
        self.art.mousePressEvent = self.press_playback

        # Creating the menu controls, media controls, and window size of the music player
        self.menu_controls()
        self.media_controls()
        self.load_saved_playlist()

    def menu_controls(self):
        """Initiate the menu bar and add it to the QMainWindow widget."""
        self.file = self.menu.addMenu('File')
        self.edit = self.menu.addMenu('Edit')
        self.playback = self.menu.addMenu('Playback')
        self.view = self.menu.addMenu('View')
        self.help_ = self.menu.addMenu('Help')

        self.file_menu()
        self.edit_menu()
        self.playback_menu()
        self.view_menu()
        self.help_menu()

    def media_controls(self):
        """Create the bottom toolbar and controls used for media playback."""
        self.addToolBar(Qt.BottomToolBarArea, self.toolbar)
        self.toolbar.setMovable(False)

        play_icon = utilities.resource_filename('mosaic.images', 'md_play.png')
        self.play_action = QAction(QIcon(play_icon), 'Play', self)
        self.play_action.triggered.connect(self.player.play)

        stop_icon = utilities.resource_filename('mosaic.images', 'md_stop.png')
        self.stop_action = QAction(QIcon(stop_icon), 'Stop', self)
        self.stop_action.triggered.connect(self.player.stop)

        previous_icon = utilities.resource_filename('mosaic.images', 'md_previous.png')
        self.previous_action = QAction(QIcon(previous_icon), 'Previous', self)
        self.previous_action.triggered.connect(self.previous)

        next_icon = utilities.resource_filename('mosaic.images', 'md_next.png')
        self.next_action = QAction(QIcon(next_icon), 'Next', self)
        self.next_action.triggered.connect(self.playlist.next)

        repeat_icon = utilities.resource_filename('mosaic.images', 'md_repeat_none.png')
        self.repeat_action = QAction(QIcon(repeat_icon), 'Repeat', self)
        self.repeat_action.setShortcut('R')
        self.repeat_action.triggered.connect(self.repeat_song)

        self.toolbar.addAction(self.play_action)
        self.toolbar.addAction(self.stop_action)
        self.toolbar.addAction(self.previous_action)
        self.toolbar.addAction(self.next_action)
        self.toolbar.addAction(self.repeat_action)
        self.toolbar.addWidget(self.slider)
        self.toolbar.addWidget(self.duration_label)

    def file_menu(self):
        """Add a file menu to the menu bar.

        The file menu houses the Open File, Open Multiple Files, Open Playlist,
        Open Directory, and Exit Application menu items.
        """
        self.open_action = QAction('Open File', self)
        self.open_action.setShortcut('O')
        self.open_action.triggered.connect(self.open_file)

        self.open_multiple_files_action = QAction('Open Multiple Files', self)
        self.open_multiple_files_action.setShortcut('M')
        self.open_multiple_files_action.triggered.connect(self.open_multiple_files)

        self.open_playlist_action = QAction('Open Playlist', self)
        self.open_playlist_action.setShortcut('CTRL+P')
        self.open_playlist_action.triggered.connect(self.open_playlist)

        self.open_directory_action = QAction('Open Directory', self)
        self.open_directory_action.setShortcut('D')
        self.open_directory_action.triggered.connect(self.open_directory)

        self.save_playlist_action = QAction('Save Playlist', self)
        self.save_playlist_action.setShortcut('CTRL+S')
        self.save_playlist_action.triggered.connect(self.save_playlist)

        self.exit_action = QAction('Quit', self)
        self.exit_action.setShortcut('CTRL+Q')
        self.exit_action.triggered.connect(self.closeEvent)

        self.file.addAction(self.open_action)
        self.file.addAction(self.open_multiple_files_action)
        self.file.addAction(self.open_playlist_action)
        self.file.addAction(self.open_directory_action)
        self.file.addSeparator()
        self.file.addAction(self.save_playlist_action)
        self.file.addSeparator()
        self.file.addAction(self.exit_action)

    def edit_menu(self):
        """Add an edit menu to the menu bar.

        The edit menu houses the preferences item that opens a preferences dialog
        that allows the user to customize features of the music player.
        """
        self.preferences_action = QAction('Preferences', self)
        self.preferences_action.setShortcut('CTRL+SHIFT+P')
        self.preferences_action.triggered.connect(lambda: self.preferences.exec_())

        self.edit.addAction(self.preferences_action)

    def playback_menu(self):
        """Add a playback menu to the menu bar.

        The playback menu houses
        """
        self.play_playback_action = QAction('Play', self)
        self.play_playback_action.setShortcut('P')
        self.play_playback_action.triggered.connect(self.player.play)

        self.stop_playback_action = QAction('Stop', self)
        self.stop_playback_action.setShortcut('S')
        self.stop_playback_action.triggered.connect(self.player.stop)

        self.previous_playback_action = QAction('Previous', self)
        self.previous_playback_action.setShortcut('B')
        self.previous_playback_action.triggered.connect(self.previous)

        self.next_playback_action = QAction('Next', self)
        self.next_playback_action.setShortcut('N')
        self.next_playback_action.triggered.connect(self.playlist.next)

        self.playback.addAction(self.play_playback_action)
        self.playback.addAction(self.stop_playback_action)
        self.playback.addAction(self.previous_playback_action)
        self.playback.addAction(self.next_playback_action)

    def view_menu(self):
        """Add a view menu to the menu bar.

        The view menu houses the Playlist, Media Library, Minimalist View, and Media
        Information menu items. The Playlist item toggles the playlist dock into and
        out of view. The Media Library items toggles the media library dock into and
        out of view. The Minimalist View item resizes the window and shows only the
        menu bar and player controls. The Media Information item opens a dialog that
        shows information relevant to the currently playing song.
        """
        self.dock_action = self.playlist_dock.toggleViewAction()
        self.dock_action.setShortcut('CTRL+ALT+P')

        self.library_dock_action = self.library_dock.toggleViewAction()
        self.library_dock_action.setShortcut('CTRL+ALT+L')

        self.minimalist_view_action = QAction('Minimalist View', self)
        self.minimalist_view_action.setShortcut('CTRL+ALT+M')
        self.minimalist_view_action.setCheckable(True)
        self.minimalist_view_action.triggered.connect(self.minimalist_view)

        self.view_media_info_action = QAction('Media Information', self)
        self.view_media_info_action.setShortcut('CTRL+SHIFT+M')
        self.view_media_info_action.triggered.connect(self.media_information_dialog)

        self.view.addAction(self.dock_action)
        self.view.addAction(self.library_dock_action)
        self.view.addSeparator()
        self.view.addAction(self.minimalist_view_action)
        self.view.addSeparator()
        self.view.addAction(self.view_media_info_action)

    def help_menu(self):
        """Add a help menu to the menu bar.

        The help menu houses the about dialog that shows the user information
        related to the application.
        """
        self.about_action = QAction('About', self)
        self.about_action.setShortcut('H')
        self.about_action.triggered.connect(lambda: about.AboutDialog().exec_())

        self.help_.addAction(self.about_action)

    def open_file(self):
        """Open the selected file and add it to a new playlist."""
        filename, success = QFileDialog.getOpenFileName(self, 'Open File', '', 'Audio (*.mp3 *.flac)', '', QFileDialog.ReadOnly)

        if success:
            file_info = QFileInfo(filename).fileName()
            playlist_item = QListWidgetItem(file_info)
            self.playlist.clear()
            self.playlist_view.clear()
            self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(filename)))
            self.player.setPlaylist(self.playlist)
            playlist_item.setToolTip(file_info)
            self.playlist_view.addItem(playlist_item)
            self.playlist_view.setCurrentRow(0)
            self.player.play()

    def open_multiple_files(self):
        """Open the selected files and add them to a new playlist."""
        filenames, success = QFileDialog.getOpenFileNames(self, 'Open Multiple Files', '', 'Audio (*.mp3 *.flac)', '', QFileDialog.ReadOnly)

        if success:
            self.playlist.clear()
            self.playlist_view.clear()
            for file in natsort.natsorted(filenames, alg=natsort.ns.PATH):
                file_info = QFileInfo(file).fileName()
                playlist_item = QListWidgetItem(file_info)
                self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(file)))
                self.player.setPlaylist(self.playlist)
                playlist_item.setToolTip(file_info)
                self.playlist_view.addItem(playlist_item)
                self.playlist_view.setCurrentRow(0)
                self.player.play()

    def open_playlist(self):
        """Load an M3U or PLS file into a new playlist."""
        playlist, success = QFileDialog.getOpenFileName(self, 'Open Playlist', '', 'Playlist (*.m3u *.pls)', '', QFileDialog.ReadOnly)

        if success:
            playlist = QUrl.fromLocalFile(playlist)
            self.playlist.clear()
            self.playlist_view.clear()
            self.playlist.load(playlist)
            self.player.setPlaylist(self.playlist)

            for song_index in range(self.playlist.mediaCount()):
                file_info = self.playlist.media(song_index).canonicalUrl().fileName()
                playlist_item = QListWidgetItem(file_info)
                playlist_item.setToolTip(file_info)
                self.playlist_view.addItem(playlist_item)

            self.playlist_view.setCurrentRow(0)
            self.player.play()

    def save_playlist(self):
        """Save the media in the playlist dock as a new M3U playlist."""
        playlist, success = QFileDialog.getSaveFileName(self, 'Save Playlist', '', 'Playlist (*.m3u)', '')
        if success:
            saved_playlist = "{}.m3u" .format(playlist)
            self.playlist.save(QUrl().fromLocalFile(saved_playlist), "m3u")

    def load_saved_playlist(self):
        """Load the saved playlist if user setting permits."""
        saved_playlist = "{}/.m3u" .format(self.playlist_location)
        if os.path.exists(saved_playlist):
            playlist = QUrl().fromLocalFile(saved_playlist)
            self.playlist.load(playlist)
            self.player.setPlaylist(self.playlist)

            for song_index in range(self.playlist.mediaCount()):
                file_info = self.playlist.media(song_index).canonicalUrl().fileName()
                playlist_item = QListWidgetItem(file_info)
                playlist_item.setToolTip(file_info)
                self.playlist_view.addItem(playlist_item)

            self.playlist_view.setCurrentRow(0)

    def open_directory(self):
        """Open the selected directory and add the files within to an empty playlist."""
        directory = QFileDialog.getExistingDirectory(self, 'Open Directory', '', QFileDialog.ReadOnly)

        if directory:
            self.playlist.clear()
            self.playlist_view.clear()
            for dirpath, __, files in os.walk(directory):
                for filename in natsort.natsorted(files, alg=natsort.ns.PATH):
                    file = os.path.join(dirpath, filename)
                    if filename.endswith(('mp3', 'flac')):
                        self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(file)))
                        playlist_item = QListWidgetItem(filename)
                        playlist_item.setToolTip(filename)
                        self.playlist_view.addItem(playlist_item)

            self.player.setPlaylist(self.playlist)
            self.playlist_view.setCurrentRow(0)
            self.player.play()

    def open_media_library(self, index):
        """Open a directory or file from the media library into an empty playlist."""
        self.playlist.clear()
        self.playlist_view.clear()

        if self.library_model.fileName(index).endswith(('mp3', 'flac')):
            self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(self.library_model.filePath(index))))
            self.playlist_view.addItem(self.library_model.fileName(index))

        elif self.library_model.isDir(index):
            directory = self.library_model.filePath(index)
            for dirpath, __, files in os.walk(directory):
                for filename in natsort.natsorted(files, alg=natsort.ns.PATH):
                    file = os.path.join(dirpath, filename)
                    if filename.endswith(('mp3', 'flac')):
                        self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(file)))
                        playlist_item = QListWidgetItem(filename)
                        playlist_item.setToolTip(filename)
                        self.playlist_view.addItem(playlist_item)

        self.player.setPlaylist(self.playlist)
        self.player.play()

    def display_meta_data(self):
        """Display the current song's metadata in the main window.

        If the current song contains metadata, its cover art is extracted and shown in
        the main window while the track number, artist, album, and track title are shown
        in the window title.
        """
        if self.player.isMetaDataAvailable():
            file_path = self.player.currentMedia().canonicalUrl().toLocalFile()
            (album, artist, title, track_number, *__, artwork) = metadata.metadata(file_path)

            try:
                self.pixmap.loadFromData(artwork)
            except TypeError:
                self.pixmap = QPixmap(artwork)

            meta_data = '{} - {} - {} - {}' .format(track_number, artist, album, title)

            self.setWindowTitle(meta_data)
            self.art.setScaledContents(True)
            self.art.setPixmap(self.pixmap)
            self.layout.addWidget(self.art)

    def initialize_playlist(self, start):
        """Display playlist and reset playback mode when media inserted into playlist."""
        if start == 0:
            if self.library_dock.isVisible():
                self.playlist_dock.setVisible(True)
                self.playlist_dock.show()
                self.playlist_dock.raise_()

            if self.playlist.playbackMode() != QMediaPlaylist.Sequential:
                self.playlist.setPlaybackMode(QMediaPlaylist.Sequential)
                repeat_icon = utilities.resource_filename('mosaic.images', 'md_repeat_none.png')
                self.repeat_action.setIcon(QIcon(repeat_icon))

    def press_playback(self, event):
        """Change the playback of the player on cover art mouse event.

        When the cover art is clicked, the player will play the media if the player is
        either paused or stopped. If the media is playing, the media is set
        to pause.
        """
        if event.button() == 1 and configuration.Playback().cover_art_playback.isChecked():
            if (self.player.state() == QMediaPlayer.StoppedState or
                    self.player.state() == QMediaPlayer.PausedState):
                self.player.play()
            elif self.player.state() == QMediaPlayer.PlayingState:
                self.player.pause()

    def seek(self, seconds):
        """Set the position of the song to the position dragged to by the user."""
        self.player.setPosition(seconds * 1000)

    def song_duration(self, duration):
        """Set the slider to the duration of the currently played media."""
        duration /= 1000
        self.duration = duration
        self.slider.setMaximum(duration)

    def song_position(self, progress):
        """Move the horizontal slider in sync with the duration of the song.

        The progress is relayed to update_duration() in order
        to display the time label next to the slider.
        """
        progress /= 1000

        if not self.slider.isSliderDown():
            self.slider.setValue(progress)

        self.update_duration(progress)

    def update_duration(self, current_duration):
        """Calculate the time played and the length of the song.

        Both of these times are sent to duration_label() in order to display the
        times on the toolbar.
        """
        duration = self.duration

        if current_duration or duration:
            time_played = QTime((current_duration / 3600) % 60, (current_duration / 60) % 60,
                                (current_duration % 60), (current_duration * 1000) % 1000)
            song_length = QTime((duration / 3600) % 60, (duration / 60) % 60, (duration % 60),
                                (duration * 1000) % 1000)

            if duration > 3600:
                time_format = "hh:mm:ss"
            else:
                time_format = "mm:ss"

            time_display = "{} / {}" .format(time_played.toString(time_format), song_length.toString(time_format))

        else:
            time_display = ""

        self.duration_label.setText(time_display)

    def set_state(self, state):
        """Change the icon in the toolbar in relation to the state of the player.

        The play icon changes to the pause icon when a song is playing and
        the pause icon changes back to the play icon when either paused or
        stopped.
        """
        if self.player.state() == QMediaPlayer.PlayingState:
            pause_icon = utilities.resource_filename('mosaic.images', 'md_pause.png')
            self.play_action.setIcon(QIcon(pause_icon))
            self.play_action.triggered.connect(self.player.pause)

        elif (self.player.state() == QMediaPlayer.PausedState or self.player.state() == QMediaPlayer.StoppedState):
            self.play_action.triggered.connect(self.player.play)
            play_icon = utilities.resource_filename('mosaic.images', 'md_play.png')
            self.play_action.setIcon(QIcon(play_icon))

    def previous(self):
        """Move to the previous song in the playlist.

        Moves to the previous song in the playlist if the current song is less
        than five seconds in. Otherwise, restarts the current song.
        """
        if self.player.position() <= 5000:
            self.playlist.previous()
        else:
            self.player.setPosition(0)

    def repeat_song(self):
        """Set the current media to repeat and change the repeat icon accordingly.

        There are four playback modes: repeat none, repeat all, repeat once, and shuffle.
        Clicking the repeat button cycles through each playback mode.
        """
        if self.playlist.playbackMode() == QMediaPlaylist.Sequential:
            self.playlist.setPlaybackMode(QMediaPlaylist.Loop)
            repeat_on_icon = utilities.resource_filename('mosaic.images', 'md_repeat_all.png')
            self.repeat_action.setIcon(QIcon(repeat_on_icon))

        elif self.playlist.playbackMode() == QMediaPlaylist.Loop:
            self.playlist.setPlaybackMode(QMediaPlaylist.CurrentItemInLoop)
            repeat_on_icon = utilities.resource_filename('mosaic.images', 'md_repeat_once.png')
            self.repeat_action.setIcon(QIcon(repeat_on_icon))

        elif self.playlist.playbackMode() == QMediaPlaylist.CurrentItemInLoop:
            self.playlist.setPlaybackMode(QMediaPlaylist.Random)
            repeat_icon = utilities.resource_filename('mosaic.images', 'md_shuffle.png')
            self.repeat_action.setIcon(QIcon(repeat_icon))

        elif self.playlist.playbackMode() == QMediaPlaylist.Random:
            self.playlist.setPlaybackMode(QMediaPlaylist.Sequential)
            repeat_icon = utilities.resource_filename('mosaic.images', 'md_repeat_none.png')
            self.repeat_action.setIcon(QIcon(repeat_icon))

    def activate_playlist_item(self, item):
        """Set the active media to the playlist item dobule-clicked on by the user."""
        current_index = self.playlist_view.row(item)
        if self.playlist.currentIndex() != current_index:
            self.playlist.setCurrentIndex(current_index)

        if self.player.state() != QMediaPlayer.PlayingState:
            self.player.play()

    def change_index(self, row):
        """Highlight the row in the playlist of the active media."""
        self.playlist_view.setCurrentRow(row)

    def minimalist_view(self):
        """Resize the window to only show the menu bar and audio controls."""
        if self.minimalist_view_action.isChecked():

            if self.playlist_dock.isVisible():
                self.playlist_dock_state = True
            if self.library_dock.isVisible():
                self.library_dock_state = True

            self.library_dock.close()
            self.playlist_dock.close()

            QTimer.singleShot(10, lambda: self.resize(500, 0))

        else:
            self.resize(defaults.Settings().window_size, defaults.Settings().window_size + 63)

            if self.library_dock_state:
                self.library_dock.setVisible(True)

            if self.playlist_dock_state:
                self.playlist_dock.setVisible(True)

    def dock_visiblity_change(self, visible):
        """Change the size of the main window when the docks are toggled."""
        if visible and self.playlist_dock.isVisible() and not self.library_dock.isVisible():
            self.resize(defaults.Settings().window_size + self.playlist_dock.width() + 6,
                        self.height())

        elif visible and not self.playlist_dock.isVisible() and self.library_dock.isVisible():
            self.resize(defaults.Settings().window_size + self.library_dock.width() + 6,
                        self.height())

        elif visible and self.playlist_dock.isVisible() and self.library_dock.isVisible():
            self.resize(defaults.Settings().window_size + self.library_dock.width() + 6,
                        self.height())

        elif (not visible and not self.playlist_dock.isVisible() and not
                self.library_dock.isVisible()):
            self.resize(defaults.Settings().window_size, defaults.Settings().window_size + 63)

    def media_information_dialog(self):
        """Show a dialog of the current song's metadata."""
        if self.player.isMetaDataAvailable():
            file_path = self.player.currentMedia().canonicalUrl().toLocalFile()
        else:
            file_path = None
        dialog = information.InformationDialog(file_path)
        dialog.exec_()

    def change_window_size(self):
        """Change the window size of the music player."""
        self.playlist_dock.close()
        self.library_dock.close()
        self.resize(defaults.Settings().window_size, defaults.Settings().window_size + 63)

    def change_media_library_path(self, path):
        """Change the media library path to the new path selected in the preferences dialog."""
        self.library_model.setRootPath(path)
        self.library_view.setModel(self.library_model)
        self.library_view.setRootIndex(self.library_model.index(path))

    def closeEvent(self, event):
        """Override the PyQt close event in order to handle save playlist on close."""
        playlist = "{}/.m3u" .format(self.playlist_location)
        if defaults.Settings().save_playlist_on_close:
            self.playlist.save(QUrl().fromLocalFile(playlist), "m3u")
        else:
            if os.path.exists(playlist):
                os.remove(playlist)
        QApplication.quit()
Ejemplo n.º 9
0
class MusicPlayer(QMainWindow):
    """MusicPlayer houses all of elements that directly interact with the main window."""
    def __init__(self, parent=None):
        """Initialize the QMainWindow widget.

        The window title, window icon, and window size are initialized here as well
        as the following widgets: QMediaPlayer, QMediaPlaylist, QMediaContent, QMenuBar,
        QToolBar, QLabel, QPixmap, QSlider, QDockWidget, QListWidget, QWidget, and
        QVBoxLayout. The connect signals for relavant widgets are also initialized.
        """
        super(MusicPlayer, self).__init__(parent)
        self.setWindowTitle('Mosaic')

        window_icon = utilities.resource_filename('mosaic.images', 'icon.png')
        self.setWindowIcon(QIcon(window_icon))
        self.resize(defaults.Settings().window_size,
                    defaults.Settings().window_size + 63)

        # Initiates Qt objects to be used by MusicPlayer
        self.player = QMediaPlayer()
        self.playlist = QMediaPlaylist()
        self.playlist_location = defaults.Settings().playlist_path
        self.content = QMediaContent()
        self.menu = self.menuBar()
        self.toolbar = QToolBar()
        self.art = QLabel()
        self.pixmap = QPixmap()
        self.slider = QSlider(Qt.Horizontal)
        self.duration_label = QLabel()
        self.playlist_dock = QDockWidget('Playlist', self)
        self.library_dock = QDockWidget('Media Library', self)
        self.playlist_view = QListWidget()
        self.library_view = library.MediaLibraryView()
        self.library_model = library.MediaLibraryModel()
        self.preferences = configuration.PreferencesDialog()
        self.widget = QWidget()
        self.layout = QVBoxLayout(self.widget)
        self.duration = 0
        self.playlist_dock_state = None
        self.library_dock_state = None

        # Sets QWidget() as the central widget of the main window
        self.setCentralWidget(self.widget)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.art.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)

        # Initiates the playlist dock widget and the library dock widget
        self.addDockWidget(defaults.Settings().dock_position,
                           self.playlist_dock)
        self.playlist_dock.setWidget(self.playlist_view)
        self.playlist_dock.setVisible(defaults.Settings().playlist_on_start)
        self.playlist_dock.setFeatures(QDockWidget.DockWidgetClosable)

        self.addDockWidget(defaults.Settings().dock_position,
                           self.library_dock)
        self.library_dock.setWidget(self.library_view)
        self.library_dock.setVisible(
            defaults.Settings().media_library_on_start)
        self.library_dock.setFeatures(QDockWidget.DockWidgetClosable)
        self.tabifyDockWidget(self.playlist_dock, self.library_dock)

        # Sets the range of the playback slider and sets the playback mode as looping
        self.slider.setRange(0, self.player.duration() / 1000)
        self.playlist.setPlaybackMode(QMediaPlaylist.Sequential)

        # OSX system menu bar causes conflicts with PyQt5 menu bar
        if sys.platform == 'darwin':
            self.menu.setNativeMenuBar(False)

        # Initiates Settings in the defaults module to give access to settings.toml
        defaults.Settings()

        # Signals that connect to other methods when they're called
        self.player.metaDataChanged.connect(self.display_meta_data)
        self.slider.sliderMoved.connect(self.seek)
        self.player.durationChanged.connect(self.song_duration)
        self.player.positionChanged.connect(self.song_position)
        self.player.stateChanged.connect(self.set_state)
        self.playlist_view.itemActivated.connect(self.activate_playlist_item)
        self.library_view.activated.connect(self.open_media_library)
        self.playlist.currentIndexChanged.connect(self.change_index)
        self.playlist.mediaInserted.connect(self.initialize_playlist)
        self.playlist_dock.visibilityChanged.connect(
            self.dock_visiblity_change)
        self.library_dock.visibilityChanged.connect(self.dock_visiblity_change)
        self.preferences.dialog_media_library.media_library_line.textChanged.connect(
            self.change_media_library_path)
        self.preferences.dialog_view_options.dropdown_box.currentIndexChanged.connect(
            self.change_window_size)
        self.art.mousePressEvent = self.press_playback

        # Creating the menu controls, media controls, and window size of the music player
        self.menu_controls()
        self.media_controls()
        self.load_saved_playlist()

    def menu_controls(self):
        """Initiate the menu bar and add it to the QMainWindow widget."""
        self.file = self.menu.addMenu('File')
        self.edit = self.menu.addMenu('Edit')
        self.playback = self.menu.addMenu('Playback')
        self.view = self.menu.addMenu('View')
        self.help_ = self.menu.addMenu('Help')

        self.file_menu()
        self.edit_menu()
        self.playback_menu()
        self.view_menu()
        self.help_menu()

    def media_controls(self):
        """Create the bottom toolbar and controls used for media playback."""
        self.addToolBar(Qt.BottomToolBarArea, self.toolbar)
        self.toolbar.setMovable(False)

        play_icon = utilities.resource_filename('mosaic.images', 'md_play.png')
        self.play_action = QAction(QIcon(play_icon), 'Play', self)
        self.play_action.triggered.connect(self.player.play)

        stop_icon = utilities.resource_filename('mosaic.images', 'md_stop.png')
        self.stop_action = QAction(QIcon(stop_icon), 'Stop', self)
        self.stop_action.triggered.connect(self.player.stop)

        previous_icon = utilities.resource_filename('mosaic.images',
                                                    'md_previous.png')
        self.previous_action = QAction(QIcon(previous_icon), 'Previous', self)
        self.previous_action.triggered.connect(self.previous)

        next_icon = utilities.resource_filename('mosaic.images', 'md_next.png')
        self.next_action = QAction(QIcon(next_icon), 'Next', self)
        self.next_action.triggered.connect(self.playlist.next)

        repeat_icon = utilities.resource_filename('mosaic.images',
                                                  'md_repeat_none.png')
        self.repeat_action = QAction(QIcon(repeat_icon), 'Repeat', self)
        self.repeat_action.triggered.connect(self.repeat_song)

        self.toolbar.addAction(self.play_action)
        self.toolbar.addAction(self.stop_action)
        self.toolbar.addAction(self.previous_action)
        self.toolbar.addAction(self.next_action)
        self.toolbar.addAction(self.repeat_action)
        self.toolbar.addWidget(self.slider)
        self.toolbar.addWidget(self.duration_label)

    def file_menu(self):
        """Add a file menu to the menu bar.

        The file menu houses the Open File, Open Multiple Files, Open Playlist,
        Open Directory, and Exit Application menu items.
        """
        self.open_action = QAction('Open File', self)
        self.open_action.setShortcut('O')
        self.open_action.triggered.connect(self.open_file)

        self.open_multiple_files_action = QAction('Open Multiple Files', self)
        self.open_multiple_files_action.setShortcut('M')
        self.open_multiple_files_action.triggered.connect(
            self.open_multiple_files)

        self.open_playlist_action = QAction('Open Playlist', self)
        self.open_playlist_action.setShortcut('CTRL+P')
        self.open_playlist_action.triggered.connect(self.open_playlist)

        self.open_directory_action = QAction('Open Directory', self)
        self.open_directory_action.setShortcut('D')
        self.open_directory_action.triggered.connect(self.open_directory)

        self.save_playlist_action = QAction('Save Playlist', self)
        self.save_playlist_action.setShortcut('CTRL+S')
        self.save_playlist_action.triggered.connect(self.save_playlist)

        self.exit_action = QAction('Quit', self)
        self.exit_action.setShortcut('CTRL+Q')
        self.exit_action.triggered.connect(self.closeEvent)

        self.file.addAction(self.open_action)
        self.file.addAction(self.open_multiple_files_action)
        self.file.addAction(self.open_playlist_action)
        self.file.addAction(self.open_directory_action)
        self.file.addSeparator()
        self.file.addAction(self.save_playlist_action)
        self.file.addSeparator()
        self.file.addAction(self.exit_action)

    def edit_menu(self):
        """Add an edit menu to the menu bar.

        The edit menu houses the preferences item that opens a preferences dialog
        that allows the user to customize features of the music player.
        """
        self.preferences_action = QAction('Preferences', self)
        self.preferences_action.setShortcut('CTRL+SHIFT+P')
        self.preferences_action.triggered.connect(
            lambda: self.preferences.exec_())

        self.edit.addAction(self.preferences_action)

    def playback_menu(self):
        """Add a playback menu to the menu bar.

        The playback menu houses
        """
        self.play_playback_action = QAction('Play', self)
        self.play_playback_action.setShortcut('P')
        self.play_playback_action.triggered.connect(self.player.play)

        self.stop_playback_action = QAction('Stop', self)
        self.stop_playback_action.setShortcut('S')
        self.stop_playback_action.triggered.connect(self.player.stop)

        self.previous_playback_action = QAction('Previous', self)
        self.previous_playback_action.setShortcut('B')
        self.previous_playback_action.triggered.connect(self.previous)

        self.next_playback_action = QAction('Next', self)
        self.next_playback_action.setShortcut('N')
        self.next_playback_action.triggered.connect(self.playlist.next)

        self.playback.addAction(self.play_playback_action)
        self.playback.addAction(self.stop_playback_action)
        self.playback.addAction(self.previous_playback_action)
        self.playback.addAction(self.next_playback_action)

    def view_menu(self):
        """Add a view menu to the menu bar.

        The view menu houses the Playlist, Media Library, Minimalist View, and Media
        Information menu items. The Playlist item toggles the playlist dock into and
        out of view. The Media Library items toggles the media library dock into and
        out of view. The Minimalist View item resizes the window and shows only the
        menu bar and player controls. The Media Information item opens a dialog that
        shows information relevant to the currently playing song.
        """
        self.dock_action = self.playlist_dock.toggleViewAction()
        self.dock_action.setShortcut('CTRL+ALT+P')

        self.library_dock_action = self.library_dock.toggleViewAction()
        self.library_dock_action.setShortcut('CTRL+ALT+L')

        self.minimalist_view_action = QAction('Minimalist View', self)
        self.minimalist_view_action.setShortcut('CTRL+ALT+M')
        self.minimalist_view_action.setCheckable(True)
        self.minimalist_view_action.triggered.connect(self.minimalist_view)

        self.view_media_info_action = QAction('Media Information', self)
        self.view_media_info_action.setShortcut('CTRL+SHIFT+M')
        self.view_media_info_action.triggered.connect(
            self.media_information_dialog)

        self.view.addAction(self.dock_action)
        self.view.addAction(self.library_dock_action)
        self.view.addSeparator()
        self.view.addAction(self.minimalist_view_action)
        self.view.addSeparator()
        self.view.addAction(self.view_media_info_action)

    def help_menu(self):
        """Add a help menu to the menu bar.

        The help menu houses the about dialog that shows the user information
        related to the application.
        """
        self.about_action = QAction('About', self)
        self.about_action.setShortcut('H')
        self.about_action.triggered.connect(
            lambda: about.AboutDialog().exec_())

        self.help_.addAction(self.about_action)

    def open_file(self):
        """Open the selected file and add it to a new playlist."""
        filename, success = QFileDialog.getOpenFileName(
            self, 'Open File', '', 'Audio (*.mp3 *.flac)', '',
            QFileDialog.ReadOnly)

        if success:
            file_info = QFileInfo(filename).fileName()
            playlist_item = QListWidgetItem(file_info)
            self.playlist.clear()
            self.playlist_view.clear()
            self.playlist.addMedia(
                QMediaContent(QUrl().fromLocalFile(filename)))
            self.player.setPlaylist(self.playlist)
            playlist_item.setToolTip(file_info)
            self.playlist_view.addItem(playlist_item)
            self.playlist_view.setCurrentRow(0)
            self.player.play()

    def open_multiple_files(self):
        """Open the selected files and add them to a new playlist."""
        filenames, success = QFileDialog.getOpenFileNames(
            self, 'Open Multiple Files', '', 'Audio (*.mp3 *.flac)', '',
            QFileDialog.ReadOnly)

        if success:
            self.playlist.clear()
            self.playlist_view.clear()
            for file in natsort.natsorted(filenames, alg=natsort.ns.PATH):
                file_info = QFileInfo(file).fileName()
                playlist_item = QListWidgetItem(file_info)
                self.playlist.addMedia(
                    QMediaContent(QUrl().fromLocalFile(file)))
                self.player.setPlaylist(self.playlist)
                playlist_item.setToolTip(file_info)
                self.playlist_view.addItem(playlist_item)
                self.playlist_view.setCurrentRow(0)
                self.player.play()

    def open_playlist(self):
        """Load an M3U or PLS file into a new playlist."""
        playlist, success = QFileDialog.getOpenFileName(
            self, 'Open Playlist', '', 'Playlist (*.m3u *.pls)', '',
            QFileDialog.ReadOnly)

        if success:
            playlist = QUrl.fromLocalFile(playlist)
            self.playlist.clear()
            self.playlist_view.clear()
            self.playlist.load(playlist)
            self.player.setPlaylist(self.playlist)

            for song_index in range(self.playlist.mediaCount()):
                file_info = self.playlist.media(
                    song_index).canonicalUrl().fileName()
                playlist_item = QListWidgetItem(file_info)
                playlist_item.setToolTip(file_info)
                self.playlist_view.addItem(playlist_item)

            self.playlist_view.setCurrentRow(0)
            self.player.play()

    def save_playlist(self):
        """Save the media in the playlist dock as a new M3U playlist."""
        playlist, success = QFileDialog.getSaveFileName(
            self, 'Save Playlist', '', 'Playlist (*.m3u)', '')
        if success:
            saved_playlist = "{}.m3u".format(playlist)
            self.playlist.save(QUrl().fromLocalFile(saved_playlist), "m3u")

    def load_saved_playlist(self):
        """Load the saved playlist if user setting permits."""
        saved_playlist = "{}/.m3u".format(self.playlist_location)
        if os.path.exists(saved_playlist):
            playlist = QUrl().fromLocalFile(saved_playlist)
            self.playlist.load(playlist)
            self.player.setPlaylist(self.playlist)

            for song_index in range(self.playlist.mediaCount()):
                file_info = self.playlist.media(
                    song_index).canonicalUrl().fileName()
                playlist_item = QListWidgetItem(file_info)
                playlist_item.setToolTip(file_info)
                self.playlist_view.addItem(playlist_item)

            self.playlist_view.setCurrentRow(0)

    def open_directory(self):
        """Open the selected directory and add the files within to an empty playlist."""
        directory = QFileDialog.getExistingDirectory(self, 'Open Directory',
                                                     '', QFileDialog.ReadOnly)

        if directory:
            self.playlist.clear()
            self.playlist_view.clear()
            for dirpath, __, files in os.walk(directory):
                for filename in natsort.natsorted(files, alg=natsort.ns.PATH):
                    file = os.path.join(dirpath, filename)
                    if filename.endswith(('mp3', 'flac')):
                        self.playlist.addMedia(
                            QMediaContent(QUrl().fromLocalFile(file)))
                        playlist_item = QListWidgetItem(filename)
                        playlist_item.setToolTip(filename)
                        self.playlist_view.addItem(playlist_item)

            self.player.setPlaylist(self.playlist)
            self.playlist_view.setCurrentRow(0)
            self.player.play()

    def open_media_library(self, index):
        """Open a directory or file from the media library into an empty playlist."""
        self.playlist.clear()
        self.playlist_view.clear()

        if self.library_model.fileName(index).endswith(('mp3', 'flac')):
            self.playlist.addMedia(
                QMediaContent(QUrl().fromLocalFile(
                    self.library_model.filePath(index))))
            self.playlist_view.addItem(self.library_model.fileName(index))

        elif self.library_model.isDir(index):
            directory = self.library_model.filePath(index)
            for dirpath, __, files in os.walk(directory):
                for filename in natsort.natsorted(files, alg=natsort.ns.PATH):
                    file = os.path.join(dirpath, filename)
                    if filename.endswith(('mp3', 'flac')):
                        self.playlist.addMedia(
                            QMediaContent(QUrl().fromLocalFile(file)))
                        playlist_item = QListWidgetItem(filename)
                        playlist_item.setToolTip(filename)
                        self.playlist_view.addItem(playlist_item)

        self.player.setPlaylist(self.playlist)
        self.player.play()

    def display_meta_data(self):
        """Display the current song's metadata in the main window.

        If the current song contains metadata, its cover art is extracted and shown in
        the main window while the track number, artist, album, and track title are shown
        in the window title.
        """
        if self.player.isMetaDataAvailable():
            file_path = self.player.currentMedia().canonicalUrl().toLocalFile()
            (album, artist, title, track_number, *__,
             artwork) = metadata.metadata(file_path)

            try:
                self.pixmap.loadFromData(artwork)
            except TypeError:
                self.pixmap = QPixmap(artwork)

            meta_data = '{} - {} - {} - {}'.format(track_number, artist, album,
                                                   title)

            self.setWindowTitle(meta_data)
            self.art.setScaledContents(True)
            self.art.setPixmap(self.pixmap)
            self.layout.addWidget(self.art)

    def initialize_playlist(self, start):
        """Display playlist and reset playback mode when media inserted into playlist."""
        if start == 0:
            if self.library_dock.isVisible():
                self.playlist_dock.setVisible(True)
                self.playlist_dock.show()
                self.playlist_dock.raise_()

            if self.playlist.playbackMode() != QMediaPlaylist.Sequential:
                self.playlist.setPlaybackMode(QMediaPlaylist.Sequential)
                repeat_icon = utilities.resource_filename(
                    'mosaic.images', 'md_repeat_none.png')
                self.repeat_action.setIcon(QIcon(repeat_icon))

    def press_playback(self, event):
        """Change the playback of the player on cover art mouse event.

        When the cover art is clicked, the player will play the media if the player is
        either paused or stopped. If the media is playing, the media is set
        to pause.
        """
        if event.button() == 1 and configuration.Playback(
        ).cover_art_playback.isChecked():
            if (self.player.state() == QMediaPlayer.StoppedState
                    or self.player.state() == QMediaPlayer.PausedState):
                self.player.play()
            elif self.player.state() == QMediaPlayer.PlayingState:
                self.player.pause()

    def seek(self, seconds):
        """Set the position of the song to the position dragged to by the user."""
        self.player.setPosition(seconds * 1000)

    def song_duration(self, duration):
        """Set the slider to the duration of the currently played media."""
        duration /= 1000
        self.duration = duration
        self.slider.setMaximum(duration)

    def song_position(self, progress):
        """Move the horizontal slider in sync with the duration of the song.

        The progress is relayed to update_duration() in order
        to display the time label next to the slider.
        """
        progress /= 1000

        if not self.slider.isSliderDown():
            self.slider.setValue(progress)

        self.update_duration(progress)

    def update_duration(self, current_duration):
        """Calculate the time played and the length of the song.

        Both of these times are sent to duration_label() in order to display the
        times on the toolbar.
        """
        duration = self.duration

        if current_duration or duration:
            time_played = QTime(
                (current_duration / 3600) % 60, (current_duration / 60) % 60,
                (current_duration % 60), (current_duration * 1000) % 1000)
            song_length = QTime((duration / 3600) % 60, (duration / 60) % 60,
                                (duration % 60), (duration * 1000) % 1000)

            if duration > 3600:
                time_format = "hh:mm:ss"
            else:
                time_format = "mm:ss"

            time_display = "{} / {}".format(time_played.toString(time_format),
                                            song_length.toString(time_format))

        else:
            time_display = ""

        self.duration_label.setText(time_display)

    def set_state(self, state):
        """Change the icon in the toolbar in relation to the state of the player.

        The play icon changes to the pause icon when a song is playing and
        the pause icon changes back to the play icon when either paused or
        stopped.
        """
        if self.player.state() == QMediaPlayer.PlayingState:
            pause_icon = utilities.resource_filename('mosaic.images',
                                                     'md_pause.png')
            self.play_action.setIcon(QIcon(pause_icon))
            self.play_action.triggered.connect(self.player.pause)

        elif (self.player.state() == QMediaPlayer.PausedState
              or self.player.state() == QMediaPlayer.StoppedState):
            self.play_action.triggered.connect(self.player.play)
            play_icon = utilities.resource_filename('mosaic.images',
                                                    'md_play.png')
            self.play_action.setIcon(QIcon(play_icon))

    def previous(self):
        """Move to the previous song in the playlist.

        Moves to the previous song in the playlist if the current song is less
        than five seconds in. Otherwise, restarts the current song.
        """
        if self.player.position() <= 5000:
            self.playlist.previous()
        else:
            self.player.setPosition(0)

    def repeat_song(self):
        """Set the current media to repeat and change the repeat icon accordingly.

        There are four playback modes: repeat none, repeat all, repeat once, and shuffle.
        Clicking the repeat button cycles through each playback mode.
        """
        if self.playlist.playbackMode() == QMediaPlaylist.Sequential:
            self.playlist.setPlaybackMode(QMediaPlaylist.Loop)
            repeat_on_icon = utilities.resource_filename(
                'mosaic.images', 'md_repeat_all.png')
            self.repeat_action.setIcon(QIcon(repeat_on_icon))

        elif self.playlist.playbackMode() == QMediaPlaylist.Loop:
            self.playlist.setPlaybackMode(QMediaPlaylist.CurrentItemInLoop)
            repeat_on_icon = utilities.resource_filename(
                'mosaic.images', 'md_repeat_once.png')
            self.repeat_action.setIcon(QIcon(repeat_on_icon))

        elif self.playlist.playbackMode() == QMediaPlaylist.CurrentItemInLoop:
            self.playlist.setPlaybackMode(QMediaPlaylist.Random)
            repeat_icon = utilities.resource_filename('mosaic.images',
                                                      'md_shuffle.png')
            self.repeat_action.setIcon(QIcon(repeat_icon))

        elif self.playlist.playbackMode() == QMediaPlaylist.Random:
            self.playlist.setPlaybackMode(QMediaPlaylist.Sequential)
            repeat_icon = utilities.resource_filename('mosaic.images',
                                                      'md_repeat_none.png')
            self.repeat_action.setIcon(QIcon(repeat_icon))

    def activate_playlist_item(self, item):
        """Set the active media to the playlist item dobule-clicked on by the user."""
        current_index = self.playlist_view.row(item)
        if self.playlist.currentIndex() != current_index:
            self.playlist.setCurrentIndex(current_index)

        if self.player.state() != QMediaPlayer.PlayingState:
            self.player.play()

    def change_index(self, row):
        """Highlight the row in the playlist of the active media."""
        self.playlist_view.setCurrentRow(row)

    def minimalist_view(self):
        """Resize the window to only show the menu bar and audio controls."""
        if self.minimalist_view_action.isChecked():

            if self.playlist_dock.isVisible():
                self.playlist_dock_state = True
            if self.library_dock.isVisible():
                self.library_dock_state = True

            self.library_dock.close()
            self.playlist_dock.close()

            QTimer.singleShot(10, lambda: self.resize(500, 0))

        else:
            self.resize(defaults.Settings().window_size,
                        defaults.Settings().window_size + 63)

            if self.library_dock_state:
                self.library_dock.setVisible(True)

            if self.playlist_dock_state:
                self.playlist_dock.setVisible(True)

    def dock_visiblity_change(self, visible):
        """Change the size of the main window when the docks are toggled."""
        if visible and self.playlist_dock.isVisible(
        ) and not self.library_dock.isVisible():
            self.resize(
                defaults.Settings().window_size + self.playlist_dock.width() +
                6, self.height())

        elif visible and not self.playlist_dock.isVisible(
        ) and self.library_dock.isVisible():
            self.resize(
                defaults.Settings().window_size + self.library_dock.width() +
                6, self.height())

        elif visible and self.playlist_dock.isVisible(
        ) and self.library_dock.isVisible():
            self.resize(
                defaults.Settings().window_size + self.library_dock.width() +
                6, self.height())

        elif (not visible and not self.playlist_dock.isVisible()
              and not self.library_dock.isVisible()):
            self.resize(defaults.Settings().window_size,
                        defaults.Settings().window_size + 63)

    def media_information_dialog(self):
        """Show a dialog of the current song's metadata."""
        if self.player.isMetaDataAvailable():
            file_path = self.player.currentMedia().canonicalUrl().toLocalFile()
        else:
            file_path = None
        dialog = information.InformationDialog(file_path)
        dialog.exec_()

    def change_window_size(self):
        """Change the window size of the music player."""
        self.playlist_dock.close()
        self.library_dock.close()
        self.resize(defaults.Settings().window_size,
                    defaults.Settings().window_size + 63)

    def change_media_library_path(self, path):
        """Change the media library path to the new path selected in the preferences dialog."""
        self.library_model.setRootPath(path)
        self.library_view.setModel(self.library_model)
        self.library_view.setRootIndex(self.library_model.index(path))

    def closeEvent(self, event):
        """Override the PyQt close event in order to handle save playlist on close."""
        playlist = "{}/.m3u".format(self.playlist_location)
        if defaults.Settings().save_playlist_on_close:
            self.playlist.save(QUrl().fromLocalFile(playlist), "m3u")
        else:
            if os.path.exists(playlist):
                os.remove(playlist)
        QApplication.quit()
Ejemplo n.º 10
0
class MainWindow(QMainWindow):
    saveStateVersion = 1

    def __init__(self):
        super().__init__()

        self.confman = ConfigFileManager()

        self.graphWidget = GraphWidget()
        self.setCentralWidget(self.graphWidget)

        self.sourcesWidget = SourcesWidget()
        self.sourcesWidget.updateRequested.connect(self.update)
        self.sourcesWidget.hide()
        self.sourcesDockWidget = QDockWidget('Sources')
        self.sourcesDockWidget.setObjectName('dock_sources')
        self.sourcesDockWidget.setWidget(self.sourcesWidget)

        self.logTextEdit = QTextEdit()
        self.logTextEdit.setReadOnly(True)
        self.logDockWidget = QDockWidget('Log')
        self.logDockWidget.setObjectName('dock_log')
        self.logDockWidget.setWidget(self.logTextEdit)

        dock_p = None
        toolDockWidgets = []
        self.toolIAD = IADToolWidget(self.graphWidget)
        self.tools = []
        self.toolWidgets = [self.toolIAD, FitToolWidget(self.graphWidget)]
        self.curTool = self.toolIAD.tool
        for t in self.toolWidgets:
            t.plotRequested.connect(self.plotRequested)
            dock = QDockWidget(t.tool.label)
            dock.setObjectName('dock_%s' % t.tool.name)
            dock.setWidget(t)
            toolDockWidgets.append(dock)
            self.tools.append(t.tool)
            self.addDockWidget(Qt.RightDockWidgetArea, dock)
            if dock_p:
                self.tabifyDockWidget(dock_p, dock)
            dock_p = dock

        self.addDockWidget(Qt.BottomDockWidgetArea, self.sourcesDockWidget)
        self.resizeDocks(toolDockWidgets, [400] * len(toolDockWidgets),
                         Qt.Horizontal)
        self.toolIAD.raise_()

        self.addDockWidget(Qt.BottomDockWidgetArea, self.logDockWidget)
        self.tabifyDockWidget(self.logDockWidget, self.sourcesDockWidget)
        self.resizeDocks([self.sourcesDockWidget, self.logDockWidget],
                         [200, 200], Qt.Vertical)
        self.logDockWidget.raise_()

        actions = [
            ('act_file_import', '&Import file',
             QKeySequence(Qt.CTRL | Qt.Key_I), self.showImportFileDialog),
            ('act_session_new', '&New session', QKeySequence.New,
             self.newSession),
            ('act_session_open', '&Open session', QKeySequence.Open,
             self.showOpenSessionDialog),
            ('act_session_save', '&Save session', QKeySequence.Save,
             self.saveSession),
            ('act_session_save_as', 'Save session as', QKeySequence.SaveAs,
             lambda: self.saveSession(True)),
        ]

        for name, label, key, func in actions:
            act = QAction(label)
            act.setShortcut(key)
            act.triggered.connect(func)
            setattr(self, name, act)

        menubar = self.menuBar()
        filemenu = menubar.addMenu('&File')
        filemenu.addAction(self.act_file_import)

        sessionmenu = menubar.addMenu('&Session')
        sessionmenu.addAction(self.act_session_new)
        sessionmenu.addAction(self.act_session_open)
        sessionmenu.addAction(self.act_session_save)
        sessionmenu.addAction(self.act_session_save_as)

        self.act_session_relative = sessionmenu.addAction(
            'Save with relative path')
        self.act_session_relative.setCheckable(True)

        viewmenu = menubar.addMenu('&View')
        for dockw in toolDockWidgets + [
                self.sourcesDockWidget, self.logDockWidget
        ]:
            viewmenu.addAction(dockw.toggleViewAction())

        self.resize(1000, 800)
        self.setAcceptDrops(True)

        self._prevSheetSet = None
        self.sessionFilename = None
        self.performanceReport = False

        try:
            self.loadConfig()
        except:
            log.warnException()
            return

    def dragEnterEvent(self, ev):
        if not ev.mimeData().hasUrls():
            return
        for url in ev.mimeData().urls():
            if url.isLocalFile():
                ev.acceptProposedAction()
                return

    def dropEvent(self, ev):
        if not ev.mimeData().hasUrls():
            return

        files = []
        for url in ev.mimeData().urls():
            if url.isLocalFile():
                files.append(url.toLocalFile())

        if files:
            ev.acceptProposedAction()
            self.importFiles(files)

    def showImportFileDialog(self):
        dlg = FileDialog('file_import')
        if dlg.exec_() == dlg.Accepted:
            self.importFiles(
                [os.path.realpath(f) for f in dlg.selectedFiles()])

    def importFiles(self, filenames):
        for filename in filenames:
            try:
                f = fileloader.load(filename)
            except fileloader.UnsupportedFileException as ex:
                logging.error('Unsupported file: %s %s' %
                              (ex.mimetype, ex.filename))
                continue
            self.addFile(filename, True, True, [(s, True) for s in f])

        if self.curTool == self.toolIAD:
            self.toolIAD.mode = 'orig'

        self.update()
        self.sourcesDockWidget.raise_()

    def addFile(self, filename, checked, expanded, sheets):
        self.sourcesWidget.addFile(filename, checked, expanded, sheets)

    def update(self, autoRange=True):
        if not self.performanceReport:
            self.update_(autoRange)

        else:
            import cProfile, pstats, io
            pr = cProfile.Profile()
            pr.enable()
            try:
                self.update_(autoRange)
            finally:
                pr.disable()
                s = io.StringIO()
                sortby = 'cumulative'
                ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
                ps.print_stats()
                logging.info('\n' + s.getvalue())

    def update_(self, autoRange=True):
        for t in self.tools:
            t.clear()

        for sw in self.sourcesWidget.enabledSheetWidgets():
            X = sw.sheet.xValues()
            Y = sw.sheet.yValues()
            Y_ = sw.sheet.yErrors()
            if len(X) == 1: X = list(X) * len(Y)
            if len(X) != len(Y):
                raise RuntimeError('X and Y formulae count mismatch')

            for i, (x, y, y_) in enumerate(zip(X, Y, Y_)):
                name = sw.sheet.name
                if len(Y) > 1: name += ':%s' % i
                for t in self.tools:
                    t.add(name, x, y, y_)

        self.updateGraph()
        if autoRange:
            self.graphWidget.autoRange()

    def plotRequested(self, tool, autoRange):
        self.curTool = tool

        sheetSet = [([
            sw.sheet.xFormula.value(),
            sw.sheet.yFormula.value(),
            sw.sheet.xRange.strValue()
        ] + sw.sheet.errors)
                    for sw in self.sourcesWidget.enabledSheetWidgets()]
        if sheetSet != self._prevSheetSet:
            logging.info('Force reload lines from sources')
            self._prevSheetSet = sheetSet
            self.update()
            return

        self.updateGraph()
        if autoRange:
            self.graphWidget.autoRange()

    def updateGraph(self):
        self.graphWidget.clearItems()
        colorpicker = self.graphWidget.getColorPicker()
        for item in self.curTool.getGraphItems(colorpicker):
            self.graphWidget.addItem(item)

    def log_(self, html, activate=False):
        self.logTextEdit.moveCursor(QTextCursor.End)
        self.logTextEdit.insertHtml(html)
        s = self.logTextEdit.verticalScrollBar()
        s.setValue(s.maximum())
        if activate:
            self.logDockWidget.raise_()

    def closeEvent(self, ev):
        self.saveConfig()
        ev.accept()

    def newSession(self):
        self.sourcesWidget.removeAllFiles()
        self.sessionFilename = None
        for tw in self.toolWidgets:
            tw.newSession()
        self.graphWidget.clear()

    def showOpenSessionDialog(self):
        dlg = FileDialog('session_open')
        if dlg.exec_() != dlg.Accepted:
            return

        filename = os.path.realpath(dlg.selectedFiles()[0])
        try:
            self.loadSession(SessionFileManager().load(filename), filename)
        except:
            log.warnException()
            return

        self.sessionFilename = filename
        self.update()

    def saveSession(self, forceShowDialog=False):
        if self.sessionFilename is None or forceShowDialog:
            dlg = FileDialog('session_save')
            if dlg.exec_() != dlg.Accepted:
                return
            self.sessionFilename = os.path.realpath(dlg.selectedFiles()[0])

        SessionFileManager().save(self.createSessionData(),
                                  self.sessionFilename)

    def loadSession(self, sess, basefilename):
        logging.debug('Loading session')

        self.sourcesWidget.removeAllFiles()

        if 'relative' in sess:
            self.act_session_relative.setChecked(sess['relative'])

        if 'files' in sess:
            from os.path import normpath, join, dirname

            for f in sess['files']:
                if basefilename:
                    filename = normpath(
                        join(dirname(basefilename), f['filename']))
                sheets = []

                try:
                    book = fileloader.load(filename)
                except:
                    log.warnException()
                    continue

                for s in f['sheets']:
                    c = s['enabled']
                    i = s['index']

                    sheet = book.getSheet(i)
                    if 'xformula' in s:
                        sheet.xFormula.setStrValue(s['xformula'])
                    if 'yformula' in s:
                        sheet.yFormula.setStrValue(s['yformula'])
                    if 'xrange' in s: sheet.xRange.setStrValue(s['xrange'])
                    if 'errors' in s: sheet.errors = s['errors']
                    sheets.append((sheet, c))

                logging.debug('Add file: %s' % filename)
                self.sourcesWidget.show()
                self.sourcesWidget.addFile(filename, f.get('enabled', True),
                                           f.get('expanded', True), sheets)

        if 'graph' in sess:
            graph = sess['graph']
            if 'range' in graph:
                self.graphWidget.setRange(QRectF(*graph['range']))

        if 'tools' in sess:
            tools = sess['tools']
            for tw in self.toolWidgets:
                if tw.tool.name in tools:
                    try:
                        tw.restoreState(tools[tw.tool.name])
                    except:
                        log.warnException()

        if 'cur_tool' in sess:
            for tw in self.toolWidgets:
                if tw.tool.name == sess['cur_tool']:
                    tw.raise_()
                    self.curTool = tw.tool

    def createSessionData(self, forceAbsPath=False):
        files = []
        relative = self.act_session_relative.isChecked()

        for f in self.sourcesWidget.files():
            if not forceAbsPath and relative:
                f['filename'] = os.path.relpath(
                    f['filename'], os.path.dirname(self.sessionFilename))

            f['sheets'] = [{
                'enabled': sc,
                'index': sw.sheet.idx,
                'xformula': sw.sheet.xFormula.strValue(),
                'yformula': sw.sheet.yFormula.strValue(),
                'xrange': sw.sheet.xRange.strValue(),
                'errors': sw.sheet.errors
            } for sw, sc in f['sheets']]

            files.append(f)

        r = self.graphWidget.viewRect()
        obj = {
            'relative':
            relative,
            'files':
            files,
            'graph': {
                'range': [r.x(), r.y(), r.width(),
                          r.height()]
            },
            'tools':
            dict([(tw.tool.name, tw.saveState()) for tw in self.toolWidgets]),
            'cur_tool':
            self.curTool.name
        }

        return obj

    def loadConfig(self):
        logging.debug('Loading config file')
        obj = self.confman.load()
        if obj is None:
            return

        if 'mainwindow' in obj:
            mw = obj['mainwindow']
            if 'state' in mw:
                try:
                    self.restoreState(b64decode(mw['state']),
                                      self.saveStateVersion)
                except:
                    log.warnException()

            if 'geometry' in mw:
                self.restoreGeometry(b64decode(mw['geometry']))

        if 'filedialogs' in obj:
            fd = obj['filedialogs']
            if 'state' in fd and fd['state'] is not None:
                FileDialog.state = b64decode(fd['state'])
            if 'states' in fd: FileDialog.states.update(fd['states'])

        if 'session' in obj:
            self.loadSession(obj['session'], self.confman.filename)

        self.update()
        self.sourcesDockWidget.raise_()

    def saveConfig(self):
        fdstate = None
        if FileDialog.state is not None:
            fdstate = str(b64encode(FileDialog.state), 'ascii')
        obj = {
            'session': self.createSessionData(True),
            'mainwindow': {
                'state':
                str(b64encode(self.saveState(self.saveStateVersion)), 'ascii'),
                'geometry':
                str(b64encode(self.saveGeometry()), 'ascii'),
            },
            'filedialogs': {
                'state': fdstate,
                'states': FileDialog.states
            }
        }
        self.confman.save(obj)