diff options
Diffstat (limited to 'hypervideo_gui')
-rw-r--r-- | hypervideo_gui/main_ui.py | 692 |
1 files changed, 373 insertions, 319 deletions
diff --git a/hypervideo_gui/main_ui.py b/hypervideo_gui/main_ui.py index 3421d9f..172eb31 100644 --- a/hypervideo_gui/main_ui.py +++ b/hypervideo_gui/main_ui.py @@ -2,74 +2,109 @@ ''' Hypervideo GUI ''' -import os -import re -import threading -import json -import time +import sys -import hypervideo +from PyQt5.QtCore import ( + QFile, + QPoint, + QRect, + QSize, + QStandardPaths, + Qt, + QProcess, + QSettings +) +from PyQt5.QtGui import QIcon, QFont, QClipboard from PyQt5.QtWidgets import ( QAction, QApplication, QComboBox, QFileDialog, - QGridLayout, - QLabel, + QHBoxLayout, QLineEdit, + QLabel, QMainWindow, QMessageBox, + QProgressBar, QPushButton, + QToolButton, + QVBoxLayout, QWidget, ) -from PyQt5.QtCore import ( - pyqtSlot, -) +# Debuging +if len(sys.argv) == 2: + if sys.argv[1] == '-v': + DEBUG = 0 + else: + DEBUG = None +else: + DEBUG = None __version__ = '0.1' -__license__ = "GPL-3" +__license__ = 'GPL-3' +__title__ = 'Simple Hypervideo Download GUI' -__path__ = os.environ['HOME'] -__pathBASE__ = r'%s/.config/hypervideo_gui/' % __path__ -if not os.path.exists(__pathBASE__): - os.makedirs(__pathBASE__) -GUI_STATE_JSON_FILE = '%s/.config/hypervideo_gui/hypervideo_gui_state.json' % __path__ +class MainWindow(QMainWindow): + def __init__(self): + ''' Initial ''' + super(MainWindow, self).__init__() + self.hypervideo_bin = None + self.url_catch = None -class App(QMainWindow): - """ - Simple applicaction for download using hypervideo - """ - _VALID_URL = r"""^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$""" + self.out_folder_path = '/tmp/' - def __init__(self): - super().__init__() + self.settings = QSettings('YouTubeDL', 'YTDL') + self.setAttribute(Qt.WA_DeleteOnClose) + self.create_status_bar() + pyfile = QStandardPaths.findExecutable("hypervideo") + if not pyfile == "": + debugging('Found executable: %s' % pyfile) + self.hypervideo_bin = pyfile + else: + self.msgbox("hypervideo not found\nPlease install hypervideo") - # load GUI state from file, load default state if save file not found - gui_state = self.load_gui_state() - self.download_folder_list = gui_state['DownloadFolderList'] self.default_formats_menu_items = ['Video/Audio - Best Quality', - 'Audio Only - Best Quality', - 'Detect All Available Formats'] + 'Audio Only - Best Quality'] - # initialize window dimensions - self.left = 100 - self.top = 100 - self.width = 640 - self.height = 200 - self.setFixedSize(self.width, self.height) + self.list = [] - self._init_ui() + self.init_ui() - def _init_ui(self): + def init_ui(self): ''' Initial UI ''' - self.setWindowTitle('Simple Hypervideo Downloader GUI') - self.setGeometry(self.left, self.top, self.width, self.height) - # Initialize status bar - self.statusBar().showMessage('Welcome to Simple Hypervideo Downloader GUI!') + + self.setWindowTitle(__title__) + + btnwidth = 155 + + self.cmd = None + self.process = QProcess(self) + self.process.started.connect(lambda: self.show_message("Creating List")) + self.process.started.connect(lambda: self.btn_get_formats.setEnabled(False)) + self.process.finished.connect(lambda: self.show_message("Finished creating List")) + self.process.finished.connect(self.process_finished) + self.process.finished.connect(lambda: self.btn_get_formats.setEnabled(True)) + self.process.readyRead.connect(self.process_output) + + self.download_process = QProcess(self) + self.download_process.setProcessChannelMode(QProcess.MergedChannels) + self.download_process.started.connect(lambda: self.show_message("Download started")) + self.download_process.started.connect(lambda: self.download_button.setEnabled(False)) + self.download_process.started.connect(lambda: self.cancel_button.setEnabled(True)) + self.download_process.finished.connect(lambda: self.show_message("Download finished")) + self.download_process.finished.connect(lambda: self.download_button.setEnabled(True)) + self.download_process.finished.connect(lambda: self.cancel_button.setEnabled(False)) + self.download_process.finished.connect(lambda: self.setWindowTitle(__title__)) + self.download_process.readyRead.connect(self.dl_process_out) + + self.setGeometry(0, 0, 600, 250) + self.setFixedSize(600, 250) + self.setStyleSheet(ui_style_sheet(self)) + self.setWindowIcon(QIcon.fromTheme("video-playlist")) # Menu main_menu = self.menuBar() @@ -90,58 +125,108 @@ class App(QMainWindow): help_menu.addAction(about_button) file_menu.addAction(exit_button) - # Create URL entry buttons and entry textbox - url_entry_label = QLabel('Enter URL:') - self.url_entry_text = QLineEdit() - self.url_entry_text.setPlaceholderText('https://invidio.us/watch?v=8SdPLG-_wtA') - # set up callback to update video formats when URL is changed - self.url_entry_text.textChanged.connect(self.reset_video_formats) - - # create output folder button and entry textbox - output_folder_button = QPushButton('Select Output Folder') - output_folder_button.setToolTip('Select output folder') - output_folder_button.clicked.connect(self.update_output_folder) - self.output_entry_combobox = QComboBox() - self.output_entry_combobox.setEditable(True) - - for item in self.download_folder_list: - # set default output folder to be downloads folder - self.output_entry_combobox.addItem(item) - # self.output_entry_combobox.editTextChanged[str].connect(self.download_text_changed) - - # add combobox for video download format and detect formats button - detect_formats_label = QLabel('Download Format:') + # Path + lbl_url = QLabel() + lbl_url.setText("Insert URL or YouTube ID:") + lbl_url.setAlignment(Qt.AlignRight) + lbl_url.setFixedWidth(btnwidth) + lbl_url.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) + self.lbl_url_path = QLineEdit() + self.lbl_url_path.setPlaceholderText('https://invidio.us/watch?v=8SdPLG-_wtA') + # Set up callback to update video formats when URL is changed + self.lbl_url_path.textChanged.connect(self.reset_video_formats) + + hlayout = QHBoxLayout() + hlayout.addWidget(lbl_url) + hlayout.addWidget(self.lbl_url_path) + + # Output path + btn_out_path = QToolButton() + btn_out_path.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + btn_out_path.setText("Select Output Folder") + btn_out_path.setFixedWidth(btnwidth) + btn_out_path.clicked.connect(self.open_output_folder) + self.lbl_out_path = QLineEdit() + self.lbl_out_path.setPlaceholderText("Insert Output Folder Path") + self.lbl_out_path.textChanged.connect(self.update_output_path) + + hlayout2 = QHBoxLayout() + hlayout2.addWidget(btn_out_path) + hlayout2.addWidget(self.lbl_out_path) + + # Hypervideo path + btn_hypervideo_path = QToolButton() + btn_hypervideo_path.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + btn_hypervideo_path.setText("Select hypervideo") + btn_hypervideo_path.setFixedWidth(btnwidth) + btn_hypervideo_path.clicked.connect(self.select_hyper_dl) + self.lbl_hypervideo_path = QLineEdit(str(self.hypervideo_bin)) + self.lbl_hypervideo_path.textChanged.connect(self.update_hypervideo_path) + self.lbl_hypervideo_path.setPlaceholderText("Insert Path to Hypervideo") + + hlayout3 = QHBoxLayout() + hlayout3.addWidget(btn_hypervideo_path) + hlayout3.addWidget(self.lbl_hypervideo_path) + + self.download_button = QToolButton() + self.download_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + self.download_button.setText("Download") + self.download_button.clicked.connect(self.download_selected) + self.download_button.setFixedWidth(btnwidth) + self.download_button.setFixedHeight(32) + + self.btn_get_formats = QToolButton() + self.btn_get_formats.setText('Get Formats') + self.btn_get_formats.setFixedWidth(btnwidth) + self.btn_get_formats.setFixedHeight(32) + self.btn_get_formats.clicked.connect(self.fill_combo_formats) + + self.cancel_button = QToolButton() + self.cancel_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + self.cancel_button.setText("Cancel") + self.cancel_button.clicked.connect(self.cancel_download) + self.cancel_button.setEnabled(False) + self.cancel_button.setFixedWidth(btnwidth) + self.cancel_button.setFixedHeight(32) self.video_format_combobox = QComboBox() - # set default values for format select combobox self.populate_video_format_combobox(self.default_formats_menu_items) - self.video_format_combobox.activated[str].connect(self.video_format_change) + self.video_format_combobox.setFixedHeight(26) - # add download button - self.download_button = QPushButton('Download') - self.download_button.clicked.connect(self.download_video_callback) + self.pbar = QProgressBar() + self.pbar.setFixedHeight(16) + self.pbar.setMaximum(100) + self.pbar.setMinimum(0) + self.pbar.setValue(0) - # create grid layout - layout = QGridLayout() + btn_layout = QHBoxLayout() + btn_layout.addWidget(self.download_button) + btn_layout.addWidget(self.btn_get_formats) + btn_layout.addWidget(self.cancel_button) - # add widgets to the layout - layout.addWidget(url_entry_label, 1, 0) - layout.addWidget(self.url_entry_text, 1, 1) - layout.addWidget(output_folder_button, 2, 0) - layout.addWidget(self.output_entry_combobox, 2, 1) - layout.addWidget(detect_formats_label, 3, 0) - layout.addWidget(self.video_format_combobox, 3, 1) - layout.addWidget(self.download_button, 5, 0) + vlayout = QVBoxLayout() + vlayout.addLayout(hlayout) + vlayout.addLayout(hlayout2) + vlayout.addLayout(hlayout3) + vlayout.addWidget(self.video_format_combobox) + vlayout.addWidget(self.pbar) + vlayout.addLayout(btn_layout) - # add grid layout as central widget for main window - main_widget = QWidget() - main_widget.setLayout(layout) - self.setCentralWidget(main_widget) + mywidget = QWidget() + mywidget.setLayout(vlayout) - self.show() + self.setCentralWidget(mywidget) + + # Copy background ID or URL of clipboard + self.clip = QApplication.clipboard() + if self.clip.text().startswith("http"): + self.lbl_url_path.setText(self.clip.text()) + self.fill_combo_formats() + + self.read_settings() def on_button_clicked(self): - """ button about """ + """ Button about """ msg = QMessageBox() msg.setWindowTitle('About us') @@ -152,154 +237,67 @@ class App(QMainWindow): msg.exec_() - def url_catch(self): - ''' Return URL EntryText''' - url = self.url_entry_text.text() - url = str(url.strip()) - return url - - def url_check_valid(self): - ''' Check valid URL ''' - if re.match(self._VALID_URL, self.url_catch()) is None: - check = False - else: - check = True - return check - - def download_video_callback(self): - ''' Callback for the "Download Video" button + def closeEvent(self, event): + '''Protected Function for PyQt5 + gets called when the user closes the GUI. ''' + self.write_settings() - def download_video_thread_helper(self, ydl_opts): - '''Download the video. Meant to be called in a background daemon thread - ''' - with hypervideo.YoutubeDL(ydl_opts) as ydl: - ydl.download([self.url_catch()]) - - self.statusBar().showMessage('Downloading Video... Done!', msecs=0) - self.download_button.setEnabled(True) - - # make sure a valid output directory was entered - if not os.path.isdir(self.output_entry_combobox.currentText()): - self.statusBar().showMessage('Invalid download directory!') - return - - # make sure the Download Folder List combobox is populated with the latest entry - # this covers the case where the user uses the edittext portion of the combobox - self.add_item_to_downloads_combobox(self.output_entry_combobox.currentText()) - - # set output path/format - outtmpl = os.path.join(self.output_entry_combobox.currentText(), r'%(title)s.%(ext)s') - - # create the youtube downloader options based on video format combobox selection - if self.video_format_combobox.currentText() == self.default_formats_menu_items[0]: - # download best video quality - ydl_opts = { - 'format': 'bestvideo+bestaudio/best', - 'outtmpl': outtmpl, - } - elif self.video_format_combobox.currentText() == self.default_formats_menu_items[1]: - # for downloading best audio and converting to mp3 - ydl_opts = { - 'format': 'bestaudio/best', - 'outtmpl': outtmpl, - 'postprocessors': [{ - 'key': 'FFmpegExtractAudio', - 'preferredcodec': 'mp3', - 'preferredquality': '192', # not actually best quality... - }], - } - else: - # grab video format from the dropdown string: - # ie. "135 - some video metadata here" -> "135" - video_format = self.video_format_combobox.currentText()[0:self.video_format_combobox.currentText().find('-')-1] - # set output path/format - outformat = os.path.join(self.output_entry_combobox.currentText(), - r'%(title)s.f%(format_id)s.%(ext)s') - ydl_opts = { - 'format': video_format, - 'outtmpl': outformat, - } - - # download the video in daemon thread - if self.url_check_valid() is False: - self.statusBar().showMessage('Please add a URL...') - else: - self.statusBar().showMessage('Downloading Video...') - self.download_button.setEnabled(False) - thread = threading.Thread(target=download_video_thread_helper, args=(self, ydl_opts, )) - thread.daemon = True - thread.start() - - def update_video_formats(self): - '''Grabs the list of available video formats in background thread and populates - video format combobox with results when complete. - ''' + close = QMessageBox() + close.setIcon(QMessageBox.Question) + close.setWindowTitle('Exit') + close.setText('You sure?') + close.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel) + close = close.exec() - def get_video_formats_thread_helper(self, url): - ''' Grabs the available video formats. Intended to be run as background thread. - ''' - self.options = { - 'socket_timeout': 30, - 'format': 'best', - 'noplaylist': True, # only download single song, not playlist - } - - if self.url_check_valid() is True: - try: - with hypervideo.YoutubeDL(self.options) as ydl: - meta = ydl.extract_info(url, download=False) - formats = meta.get('formats', [meta]) - except TypeError: - self.statusBar().showMessage('Problem downloading %s' % url) - return None - - # Search formats - if formats is None: - self.statusBar().showMessage('Formats not found') - else: - item_list = self.default_formats_menu_items[0:2] - # ForLoop search formats - for quality in formats: - format_id = quality.get('format_id', '') - ext = quality.get('ext', '') - acodec = quality.get('acodec', '') - vcodec = quality.get('vcodec', '') - item_list.append('%s - %s (audio: %s) (video: %s)' % - (format_id, ext, acodec, vcodec)) - # Replace value empty in audio or video - format_list = [w.replace('(audio: )', '(audio: none)') - .replace('(video: )', '(video: none)') - for w in item_list] - # Add formats in combobox - self.populate_video_format_combobox(format_list) - self.video_format_combobox.setCurrentIndex(0) - time.sleep(.300) - self.statusBar().showMessage('Finished Downloading Video Formats', msecs=0) - - # check if is valid url - # should probably be reworked to be compatible with non-YouTube websites - if self.url_check_valid() is False: - self.populate_video_format_combobox(self.default_formats_menu_items) - return + if close == QMessageBox.Yes: + event.accept() else: - # valid url - fetch the video formats in background daemon thread - self.statusBar().showMessage('Downloading Video Formats') - thread = threading.Thread(target=get_video_formats_thread_helper, - args=(self, self.url_catch(), )) - thread.daemon = True - thread.start() - - def video_format_change(self, text): - ''' Video Format Change ''' - if text == self.default_formats_menu_items[2]: - # detect video formats was selected - # update statusbar to let user know something is happening - if self.url_check_valid() is False: - self.statusBar().showMessage('Please add a URL...') - else: - # update video formats - self.update_video_formats() + event.ignore() + + def read_settings(self): + ''' Read config ''' + debugging('Reading settings') + if self.settings.contains('geometry'): + self.setGeometry(self.settings.value('geometry')) + if self.settings.contains('outFolder'): + self.lbl_out_path.setText(self.settings.value('outFolder')) + + def write_settings(self): + ''' Save Settings ''' + debugging('Writing settings') + self.settings.setValue('outFolder', self.out_folder_path) + self.settings.setValue('geometry', self.geometry()) + + def update_output_path(self): + ''' Update Path Output ''' + self.out_folder_path = self.lbl_out_path.text() + self.show_message("Output path changed to: %s" % self.lbl_out_path.text()) + + def update_hypervideo_path(self): + ''' Update Hypervideo Path Output ''' + self.hypervideo_bin = self.lbl_hypervideo_path.text() + self.show_message("hypervideo path changed to: %s" % self.lbl_hypervideo_path.text()) + + def show_message(self, message): + ''' Show Message in StatuBar ''' + self.statusBar().showMessage(message, 0) + + def select_hyper_dl(self): + ''' Select hypervideo executable ''' + file_name, _ = QFileDialog.getOpenFileName(self, "locate hypervideo", + "/usr/bin/hypervideo", "exec Files (*)") + if file_name: + self.lbl_hypervideo_path.setText(file_name) + self.hypervideo_bin = file_name + + def open_output_folder(self): + ''' Open out folder path ''' + dlg = QFileDialog() + dlg.setFileMode(QFileDialog.Directory) + d_path = dlg.getExistingDirectory() + if d_path: + self.lbl_out_path.setText(d_path) def populate_video_format_combobox(self, labels): '''Populate the video format combobox with video formats. Clear the previous labels. @@ -321,99 +319,155 @@ class App(QMainWindow): else: self.video_format_combobox.setCurrentIndex(idx) - @pyqtSlot() - def update_output_folder(self): - ''' Callback for "Update Output Folder" button. Allows user to select - output directory via standard UI. - https://stackoverflow.com/questions/43121340/why-is-the-use-of-lensequence-in-condition-values-considered-incorrect-by-pyli - ''' - file = str(QFileDialog.getExistingDirectory(self, "Select Directory")) + def fill_combo_formats(self): + ''' Scan formats and Add item to combobox ''' + self.video_format_combobox.clear() - if file: - self.add_item_to_downloads_combobox(file) - else: - self.statusBar().showMessage('Select a folder!') - - def download_text_changed(self, text): - ''' download text changed ''' - # function not used right now - if text not in self.download_folder_list and os.path.isdir(text): - self.add_item_to_downloads_combobox(text) - - def add_item_to_downloads_combobox(self, text): - ''' Add item to download combobox ''' - if text not in self.download_folder_list: - # if it's not in the list, add it to the list - self.download_folder_list = [text]+self.download_folder_list + if QFile.exists(self.hypervideo_bin): + # Default options + self.video_format_combobox.addItems(self.default_formats_menu_items[0:2]) + self.list = [] + self.url_catch = self.lbl_url_path.text() + if not self.lbl_url_path.text() == "": + debugging('Scan Formats') + self.process.start(self.hypervideo_bin, ['-F', self.url_catch]) + else: + self.show_message("URL empty") else: - # if it's in the list already, move it to the top of the list - self.download_folder_list = [text]+[folder for folder in self.download_folder_list if not folder == text] - - # maximum download folder history of 5 - if len(self.download_folder_list) > 5: - self.download_folder_list = self.download_folder_list[0:6] - - # update the combobox - self.output_entry_combobox.clear() - for item in self.download_folder_list: - self.output_entry_combobox.addItem(item) - - self.output_entry_combobox.setCurrentIndex(0) # reset index - just in case - - def save_gui_state(self): - ''' Save GUI State ''' - save_dict = {'DownloadFolderList': self.download_folder_list, } - - if not GUI_STATE_JSON_FILE: - newf = open(GUI_STATE_JSON_FILE, "a+") - newf.close() - - with open(GUI_STATE_JSON_FILE, 'w') as file: - json.dump(save_dict, file) - - def load_gui_state(self): - ''' Load GUI state ''' - if os.path.isfile(GUI_STATE_JSON_FILE): - with open(GUI_STATE_JSON_FILE, 'r') as file: - save_data = json.load(file) - - if isinstance(save_data['DownloadFolderList'], str): - # convert to list - save_data['DownloadFolderList'] = [save_data['DownloadFolderList']] - + self.show_message("hypervideo missing") + + def process_output(self): + ''' Process out ''' + try: + output = str(self.process.readAll(), encoding='utf8').rstrip() + except TypeError: + output = str(self.process.readAll()).rstrip() + self.list.append(output) + + def process_finished(self): + ''' Process Finished ''' + out = ','.join(self.list) + out = out.partition("resolution note")[2] + out = out.partition('\n')[2] + mylist = out.rsplit('\n') + self.video_format_combobox.addItems(mylist) + count = self.video_format_combobox.count() + self.video_format_combobox.setCurrentIndex(count-1) + + def download_selected(self): + ''' Download selected video format ''' + if QFile.exists(self.hypervideo_bin): + self.pbar.setValue(0) + self.url_catch = self.lbl_url_path.text() + quality = None + if self.video_format_combobox.currentText() == self.default_formats_menu_items[0]: + quality = 'bestvideo+bestaudio/best' + options = [] + options.append('-f') + options.append(quality) + elif self.video_format_combobox.currentText() == self.default_formats_menu_items[1]: + quality = '--audio-quality' + options = [] + options.append('-x') + options.append('--audio-format') + options.append('mp3') + options.append(quality) + options.append('192') + else: + quality = self.video_format_combobox.currentText().partition(" ")[0] + options = [] + options.append('-f') + options.append(quality) + if self.url_catch != '': + if quality is not None: + options.append("-o") + options.append("%(title)s.%(ext)s") + options.append(self.url_catch) + self.show_message("Download started") + debugging('Download Selected Quality: %s' % quality) + debugging('Download URL: %s' % self.url_catch) + self.download_process.setWorkingDirectory(self.out_folder_path) + self.download_process.start(self.hypervideo_bin, options) + else: + self.show_message("List of available files is empty") + else: + self.show_message("URL empty") else: - save_data = {'DownloadFolderList': [get_default_download_path()], } - - return save_data - - def closeEvent(self, event): - '''Protected Function for PyQt5 - gets called when the user closes the GUI. - It saves the GUI state to the json file - GUI_STATE_JSON_FILE. - ''' - self.save_gui_state() - - close = QMessageBox() - close.setIcon(QMessageBox.Question) - close.setWindowTitle('Exit') - close.setText('You sure?') - close.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel) - close = close.exec() - - if close == QMessageBox.Yes: - event.accept() + self.show_message("hypervideo missing") + + def dl_process_out(self): + ''' Download process out ''' + try: + out = str(self.download_process.readAll(), encoding='utf8').rstrip() + except TypeError: + out = str(self.download_process.readAll()).rstrip() + out = out.rpartition("[download] ")[2] + self.show_message("Progress: %s" % out) + self.setWindowTitle(out) + out = out.rpartition("%")[0].rpartition(".")[0] + if not out == "": + try: + pout = int(out) + self.pbar.setValue(pout) + except ValueError: + pass + + def cancel_download(self): + ''' Cancel download''' + if self.download_process.state() == QProcess.Running: + debugging('Process is running, will be cancelled') + self.download_process.close() + self.show_message("Download cancelled") + self.pbar.setValue(0) + self.cancel_button.setEnabled(False) else: - event.ignore() - - -def get_default_download_path(): - """Returns the path for the "Downloads" folder in GNU - Used as default directory for videos to be saved to. - #From: https://stackoverflow.com/questions/35851281/python-finding-the-users-downloads-folder - """ - if os.name == 'posix': - dpath = '/tmp/hypervideo-gui/' - if not os.path.exists(dpath): - os.makedirs(dpath) - return os.path.join(dpath) + self.show_message("Process is not running") + + def create_status_bar(self): + ''' Create StatusBar''' + self.statusBar().showMessage("Ready") + + def msgbox(self, message): + ''' MessageBox''' + QMessageBox.warning(self, "Message", message) + + +def debugging(var): + ''' Debugging ''' + if DEBUG == 0: + message_debug = print('[debug] %s' % var) + else: + message_debug = None + return message_debug + +def ui_style_sheet(self): + return """ +QStatusBar +{ +font-family: Helvetica; +font-size: 8pt; +color: #666666; +} +QProgressBar:horizontal { +border: 1px solid gray; +text-align: top; +padding: 1px; +border-radius: 3px; +background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, +stop: 0 #fff, +stop: 0.4999 #eee, +stop: 0.5 #ddd, +stop: 1 #eee ); +width: 15px; +} +QProgressBar::chunk:horizontal +{ +background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, +stop: 0 #5baaf5, +stop: 0.4999 #4ba6f5, +stop: 0.5 #3ba6f5, +stop: 1 #00aaff ); +border-radius: 3px; +border: 1px solid black; +} +""" |