diff --git a/build_installers.py b/build_installers.py new file mode 100644 index 00000000..b0c96871 --- /dev/null +++ b/build_installers.py @@ -0,0 +1,18 @@ +from virtualenv.run import cli_run as virtualenv +from xicam.gui.cammart.venvs import activate_this +from pip._internal import main as pip +import subprocess + +if __name__ == '__main__': + + # create clean build venv + virtualenv(['build-venv', '--clear', '--copies']) + + # activate that venv + activate_this('build-venv') + + # install xicam and its dependencies in the venv + pip(['install', '.']) + + # Run NSIS to make an installer; check that its successful + assert not subprocess.call(['makensis', 'installer.nsi']) diff --git a/installer.nsi b/installer.nsi new file mode 100644 index 00000000..c1b50213 --- /dev/null +++ b/installer.nsi @@ -0,0 +1,137 @@ +; Note: This script must be located at a top-level above all xi-cam repos. You'll have to manually copy it one directory up. + +; ------------------------------- +; Start + !include MUI2.nsh + !include x64.nsh + !define VERSION "2.0.1" + !define MUI_PRODUCT "Xi-cam" + !define MUI_FILE "xicam" + !define MUI_BRANDINGTEXT "Xi-cam ${VERSION}" + CRCCheck On + + ; We should test if we must use an absolute path + ;!include "${NSISDIR}\Contrib\Modern UI\System.nsh" + + +;--------------------------------- +;General + + Name "Xi-cam ${VERSION}" + OutFile "Xi-cam-${VERSION}-amd64.exe" + ;ShowInstDetails "nevershow" + ;ShowUninstDetails "nevershow" + ;SetCompressor "bzip2" + + !define MUI_INSTFILESPAGE_COLORS "FFFFFF 000000" ;Two colors + !define MUI_PAGE_HEADER_TEXT "Xi-cam ${Version} Installation:" + !define MUI_ICON "xicam\gui\static\icons\xicam.ico" + !define MUI_UNICON "xicam\gui\static\icons\xicam.ico" + ;!define MUI_SPECIALBITMAP "Bitmap.bmp" + + +;-------------------------------- +;Variables + + Var StartMenuFolder + + +;-------------------------------- +;Folder selection page + + InstallDir "$PROGRAMFILES64\${MUI_PRODUCT}" + + +;-------------------------------- +;Modern UI Configuration + + !insertmacro MUI_PAGE_WELCOME + !insertmacro MUI_PAGE_LICENSE "LICENSE.md" + !insertmacro MUI_PAGE_DIRECTORY + !insertmacro MUI_PAGE_STARTMENU "Application" $StartMenuFolder + !insertmacro MUI_PAGE_INSTFILES + !insertmacro MUI_UNPAGE_WELCOME + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_LICENSE "LICENSE.md" + !insertmacro MUI_UNPAGE_COMPONENTS + !insertmacro MUI_UNPAGE_DIRECTORY + !insertmacro MUI_UNPAGE_INSTFILES + !insertmacro MUI_UNPAGE_FINISH + !insertmacro MUI_PAGE_FINISH + +; !define MUI_COMPONENTSPAGE_SMALLDESC ;No value + + +;-------------------------------- +;Language + + !insertmacro MUI_LANGUAGE "English" + + +;-------------------------------- +;Data + + LicenseData "LICENSE.md" + + +;-------------------------------- +;Installer Sections +Section "Install" + +;Add files + SetOutPath "$INSTDIR" + + File /r build-venv\* + +;create desktop shortcut + CreateShortCut "$DESKTOP\${MUI_PRODUCT}.lnk" "$INSTDIR\Scripts\${MUI_FILE}.exe" "" "$INSTDIR\Lib\site-packages\${MUI_ICON}" 0 + +;create start-menu items + CreateDirectory "$SMPROGRAMS\${MUI_PRODUCT}" + CreateShortCut "$SMPROGRAMS\${MUI_PRODUCT}\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Lib\site-packages\${MUI_ICON}" 0 + CreateShortCut "$SMPROGRAMS\${MUI_PRODUCT}\${MUI_PRODUCT}.lnk" "$INSTDIR\Scripts\${MUI_FILE}.exe" "" "$INSTDIR\Lib\site-packages\${MUI_ICON}" 0 + +;write uninstall information to the registry + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${MUI_PRODUCT}" "DisplayName" "${MUI_PRODUCT} (remove only)" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${MUI_PRODUCT}" "UninstallString" "$INSTDIR\Uninstall.exe" + + WriteUninstaller "$INSTDIR\Uninstall.exe" + +SectionEnd + + +;-------------------------------- +;Uninstaller Section +Section "Uninstall" + +;Delete Files + RMDir /r "$INSTDIR\*.*" + +;Remove the installation directory + RMDir "$INSTDIR" + +;Delete Start Menu Shortcuts + Delete "$DESKTOP\${MUI_PRODUCT}.lnk" + Delete "$SMPROGRAMS\${MUI_PRODUCT}\*.*" + RmDir "$SMPROGRAMS\${MUI_PRODUCT}" + +;Delete Uninstaller And Unistall Registry Entries + DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\${MUI_PRODUCT}" + DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${MUI_PRODUCT}" + +SectionEnd + + +;-------------------------------- +;MessageBox Section + + +;Function that calls a messagebox when installation finished correctly +Function .onInstSuccess + MessageBox MB_OK "You have successfully installed ${MUI_PRODUCT}. Use the desktop icon to start the program." +FunctionEnd + +Function un.onUninstSuccess + MessageBox MB_OK "You have successfully uninstalled ${MUI_PRODUCT}." +FunctionEnd + diff --git a/setup.cfg b/setup.cfg index 4ea1c2c5..8be7d9b5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,8 +5,8 @@ description-file = README.rst VCS = git style = pep440 versionfile_source = xicam/_version.py -versionfile_build = -tag_prefix = +versionfile_build = xicam/_version.py +tag_prefix = v [flake8] ;ignore = E226,E302,E41 diff --git a/setup.py b/setup.py index a64603b0..d8dc2a8d 100644 --- a/setup.py +++ b/setup.py @@ -113,7 +113,8 @@ # "scripts" keyword. Entry points provide cross-platform support and allow # pip to create the appropriate form of executable for the target platform. entry_points={ - "gui_scripts": ["xicam=xicam.run_xicam:main"], + "gui_scripts": ["xicam=xicam.run_xicam:main", + "splash_xicam=xicam.gui.windows.splash:main"], "xicam.plugins.DataHandlerPlugin": ["npy = xicam.core.formats.NPYPlugin:NPYPlugin"], "xicam.plugins.PluginType": [ "CatalogPlugin = xicam.plugins.catalogplugin:CatalogPlugin", diff --git a/xicam/gui/static/icons/xicam.ico b/xicam/gui/static/icons/xicam.ico index d623b6f6..366cc888 100644 Binary files a/xicam/gui/static/icons/xicam.ico and b/xicam/gui/static/icons/xicam.ico differ diff --git a/xicam/gui/static/images/animated-logo.mp4 b/xicam/gui/static/images/animated-logo.mp4 new file mode 100644 index 00000000..46f8393c Binary files /dev/null and b/xicam/gui/static/images/animated-logo.mp4 differ diff --git a/xicam/gui/static/images/animated_logo.gif b/xicam/gui/static/images/animated_logo.gif deleted file mode 100644 index 7b56d83f..00000000 Binary files a/xicam/gui/static/images/animated_logo.gif and /dev/null differ diff --git a/xicam/gui/windows/mainwindow.py b/xicam/gui/windows/mainwindow.py index 5956bcbe..9367270a 100644 --- a/xicam/gui/windows/mainwindow.py +++ b/xicam/gui/windows/mainwindow.py @@ -44,7 +44,9 @@ def __init__(self): super(XicamMainWindow, self).__init__() # Set icon - self.setWindowIcon(QIcon(QPixmap(str(path("icons/xicam.gif"))))) + xicam_icon = QIcon(QPixmap(str(path("icons/xicam.ico")))) + self.setWindowIcon(xicam_icon) + QApplication.instance().setWindowIcon(xicam_icon) # Set size and position self.setGeometry(0, 0, 1000, 600) diff --git a/xicam/gui/windows/splash.py b/xicam/gui/windows/splash.py index a486254d..efc4ce91 100644 --- a/xicam/gui/windows/splash.py +++ b/xicam/gui/windows/splash.py @@ -1,7 +1,11 @@ import sys + from qtpy.QtCore import QTimer, Qt -from qtpy.QtGui import QMovie, QPixmap -from qtpy.QtWidgets import QSplashScreen, QApplication +from qtpy.QtWidgets import QWidget, QLabel, QSizePolicy +from qtpy.QtMultimedia import QMediaPlayer, QMediaPlaylist, QMediaContent +from qtpy.QtMultimediaWidgets import QVideoWidget +from qtpy.QtCore import QUrl +from qtpy.QtWidgets import QApplication, QVBoxLayout, QMainWindow from xicam.gui import static @@ -12,10 +16,10 @@ def elide(s: str, max_len: int = 60): return s -class XicamSplashScreen(QSplashScreen): +class XicamSplashScreen(QMainWindow): minsplashtime = 5000 - def __init__(self, log_path: str, initial_length: int, f: int = Qt.WindowStaysOnTopHint | Qt.SplashScreen): + def __init__(self, log_path: str, initial_length: int): """ A QSplashScreen customized to display an animated gif. The splash triggers launch when clicked. @@ -27,25 +31,42 @@ def __init__(self, log_path: str, initial_length: int, f: int = Qt.WindowStaysOn Path to the Xi-CAM log file to reflect initial_length: int Length in bytes to seek forward before reading - f : int - Extra flags (see base class) """ - # Get logo movie from relative path - self.movie = QMovie(str(static.path("images/animated_logo.gif"))) + super(XicamSplashScreen, self).__init__(flags=Qt.WindowStaysOnTopHint | Qt.SplashScreen) + + self.videoWidget = QVideoWidget() + self.videoWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.VideoSurface) + playlist = QMediaPlaylist(self.mediaPlayer) + playlist.setPlaybackMode(QMediaPlaylist.Loop) + playlist.addMedia(QMediaContent(QUrl.fromLocalFile(str(static.path("images/animated-logo.mp4"))))) + playlist.setCurrentIndex(1) + self.mediaPlayer.setPlaylist(playlist) + self.mediaPlayer.setVideoOutput(self.videoWidget) + self.mediaPlayer.play() + self.mediaPlayer.mediaChanged.connect(self.execlaunch) + + + w = QWidget() + self.setCentralWidget(w) + self._layout = QVBoxLayout() + w.setLayout(self._layout) + w.layout().addWidget(self.videoWidget) + self.messageWidget = QLabel() + self.setStyleSheet('background-color:black; color:grey;') + w.layout().addWidget(self.messageWidget) + w.layout().setContentsMargins(10,30,10,10) + # w.layout().setSpacing(0) + self.setFixedSize(550, 400) + + self.setWindowFlags(Qt.FramelessWindowHint) # Setup drawing - self.movie.frameChanged.connect(self.paintFrame) - self.movie.jumpToFrame(1) - self.pixmap = QPixmap(self.movie.frameRect().size()) - super(XicamSplashScreen, self).__init__(self.pixmap, f) - self.setMask(self.pixmap.mask()) - self.movie.finished.connect(self.restartmovie) - self.showMessage("Starting Xi-CAM...") + self.messageWidget.setText("Starting Xi-CAM...") self._launching = False self._launchready = False - self.timer = QTimer(self) self.log_file = open(log_path, "r") self.log_file.seek(initial_length) @@ -58,20 +79,29 @@ def __init__(self, log_path: str, initial_length: int, f: int = Qt.WindowStaysOn QApplication.instance().setActiveWindow(self) # Setup timed triggers for launching the QMainWindow - self.timer.singleShot(self.minsplashtime, self.launchwindow) + self.finish_timer = QTimer(self) + self.finish_timer.singleShot(self.minsplashtime, self.execlaunch) + + self.message_timer = QTimer(self) + self.message_timer.timeout.connect(self.showMessageFromLog) + self.message_timer.start(1./60) + + def showMessageFromLog(self): + line = self.log_file.readline().strip() + if line: + self.showMessage(elide(line.split(">")[-1])) - def showMessage(self, message: str, color=Qt.darkGray): + def showMessage(self, message: str): # attempt to parse out everyting besides the message try: message=message.split(" - ")[-1] except Exception: pass else: - super(XicamSplashScreen, self).showMessage(elide(message), color=color, alignment=Qt.AlignBottom) + self.messageWidget.setText(elide(message)) def mousePressEvent(self, *args, **kwargs): - # TODO: Apparently this doesn't work? - self.timer.stop() + self.finish_timer.stop() self.execlaunch() def show(self): @@ -79,38 +109,6 @@ def show(self): Start the animation when shown """ super(XicamSplashScreen, self).show() - self.movie.start() - - def paintFrame(self): - """ - Paint the current frame - """ - self.pixmap = self.movie.currentPixmap() - self.setMask(self.pixmap.mask()) - self.setPixmap(self.pixmap) - self.movie.setSpeed(self.movie.speed() + 20) - - line = self.log_file.readline().strip() - if line: - self.showMessage(elide(line.split(">")[-1])) - - def sizeHint(self): - return self.movie.scaledSize() - - def restartmovie(self): - """ - Once the animation reaches the end, check if its time to launch, otherwise restart animation - """ - if self._launchready: - self.execlaunch() - return - self.movie.start() - - def launchwindow(self): - """ - Save state, defer launch until animation finishes - """ - self._launchready = True def execlaunch(self): """ @@ -119,16 +117,21 @@ def execlaunch(self): if not self._launching: self._launching = True - self.timer.stop() + self.finish_timer.stop() # Stop splashing self.hide() - self.movie.stop() self.close() QApplication.instance().quit() -if __name__ == "__main__": +def main(): qapp = QApplication([]) splash = XicamSplashScreen(sys.argv[-2], int(sys.argv[-1])) + # splash = XicamSplashScreen('C:\\Users\LBL\\AppData\\Local\\CAMERA\\xicam\\Cache\\logs\\out.log', 1) + splash.show() qapp.exec_() + + +if __name__ == "__main__": + main() diff --git a/xicam/run_xicam.py b/xicam/run_xicam.py index 437b88f8..d9fe9c66 100644 --- a/xicam/run_xicam.py +++ b/xicam/run_xicam.py @@ -12,14 +12,6 @@ root = os.path.dirname(sys.argv[0]) sys.path = [path for path in sys.path if os.path.abspath(root) in os.path.abspath(path)] -# Quickly extract zip file to make imports easier -if ".zip/" in os.__file__: - import zipfile - - zip_ref = zipfile.ZipFile(os.path.dirname(os.__file__), "r") - zip_ref.extractall(os.path.dirname(os.path.dirname(os.__file__))) - zip_ref.close() - os.environ["QT_API"] = "pyqt5" import qtpy from qtpy.QtWidgets import QApplication, QErrorMessage @@ -54,16 +46,15 @@ def _main(args, exec=True): app = QApplication.instance() or QApplication([]) signal.signal(signal.SIGINT, signal.SIG_DFL) - from xicam.gui.windows import splash from xicam.core import msg + from xicam.gui.windows import splash if getattr(args, 'verbose', False): QErrorMessage.qtHandler() # start splash in subprocess splash_proc = QProcess() - # splash_proc.started.connect(lambda: print('started splash')) - # splash_proc.finished.connect(lambda: print('finished splashing')) + log_file = msg.file_handler.baseFilename initial_length = os.path.getsize(log_file) splash_proc.start(sys.executable, [splash.__file__, log_file, str(initial_length)])