Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

464

465

466

467

468

469

470

471

472

473

474

475

476

477

478

479

480

481

482

483

484

485

486

487

488

489

490

491

492

493

494

495

496

497

498

499

500

501

502

503

504

505

506

507

508

509

510

511

512

513

514

515

516

517

518

519

520

521

522

523

524

525

526

527

528

529

530

531

532

533

534

535

536

537

538

539

540

541

542

543

544

545

546

547

548

549

550

551

552

553

554

555

556

557

558

559

#! /usr/bin/env python3 

# -*- coding: utf-8 -*- 

 

#       Copyright 2011-2013, 2017, Marten de Vries 

#       Copyright 2011, 2017, Milan Boers 

# 

#       This file is part of OpenTeacher. 

# 

#       OpenTeacher is free software: you can redistribute it and/or modify 

#       it under the terms of the GNU General Public License as published by 

#       the Free Software Foundation, either version 3 of the License, or 

#       (at your option) any later version. 

# 

#       OpenTeacher is distributed in the hope that it will be useful, 

#       but WITHOUT ANY WARRANTY; without even the implied warranty of 

#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 

#       GNU General Public License for more details. 

# 

#       You should have received a copy of the GNU General Public License 

#       along with OpenTeacher.  If not, see <http://www.gnu.org/licenses/>. 

 

import sys 

import os 

import platform 

import logging 

import warnings 

 

qtLogger = logging.getLogger("qt") 

 

class Action: 

        """A high-level interface to a menu and/or a toolbar item.""" 

 

        def __init__(self, createEvent, qtMenu, qtAction, *args, **kwargs): 

                super().__init__(*args, **kwargs) 

 

                self._qtMenu = qtMenu 

                self._qtAction = qtAction 

 

                self.triggered = createEvent() 

                self.toggled = createEvent() 

                #lambda to prevent useless Qt arguments to pass 

exit                self._qtAction.triggered.connect(lambda: self.triggered.send()) 

                self._qtAction.toggled.connect(self.toggled.send) 

 

        def remove(self): 

                self._qtMenu.removeAction(self._qtAction) 

                self._qtMenu.menuActions.remove(self._qtAction) 

 

exit   exit        text = property( 

                lambda self: self._qtAction.text(), 

                lambda self, value: self._qtAction.setText(value) 

        ) 

 

exit   exit        enabled = property( 

                lambda self: self._qtMenu.isEnabled(), 

                lambda self, value: self._qtAction.setEnabled(value) 

        ) 

 

class Menu: 

        """A high-level interface to a menu (as in File, Edit, etc.).""" 

 

        def __init__(self, event, qtMenu, *args, **kwargs): 

                super().__init__(*args, **kwargs) 

 

                self._createEvent = event 

                self._qtMenu = qtMenu 

                self._qtMenu.menuActions = set() 

 

        def _actionAfter(self, priority): 

                actions = sorted( 

                        self._qtMenu.menuActions, 

                        key=lambda a: getattr(a, "menuPriority", 0) 

                ) 

                for action in actions: 

                        if getattr(action, "menuPriority", 0) > priority: 

                                return action 

                #explicit is better than implicit 

                return None 

 

        def addAction(self, priority): 

                qtAction = QtWidgets.QAction(self._qtMenu) 

                qtAction.menuPriority = priority 

                self._qtMenu.insertAction(self._actionAfter(priority), qtAction) 

                self._qtMenu.menuActions.add(qtAction) 

                return Action(self._createEvent, self._qtMenu, qtAction) 

 

        def addMenu(self, priority): 

                qtSubMenu = QtWidgets.QMenu() 

                self._qtMenu.insertMenu(self._actionAfter(priority), qtSubMenu) 

                return Menu(self._createEvent, qtSubMenu) 

 

        def remove(self): 

                self._qtMenu.hide() 

 

exit   exit        text = property( 

                lambda self: self._qtMenu.title(), 

                lambda self, value: self._qtMenu.setTitle(value) 

        ) 

 

exit   exit        enabled = property( 

                lambda self: self._qtMenu.isEnabled(), 

                lambda self, value: self._qtMenu.setEnabled(value) 

        ) 

 

class StatusViewer: 

        """A high-level interface to the status bar.""" 

        def __init__(self, statusBar, *args, **kwargs): 

                super().__init__(*args, **kwargs) 

 

                self._statusBar = statusBar 

 

        def show(self, message): 

                self._statusBar.showMessage(message) 

 

class FileTab: 

        def __init__(self, moduleManager, tabWidget, widget, lastWidget, *args, **kwargs): 

                super().__init__(*args, **kwargs) 

 

                self._modules = next(iter(moduleManager.mods(type="modules"))) 

 

                self._tabWidget = tabWidget 

                self._widget = widget 

                self._lastWidget = lastWidget 

 

                self.closeRequested = self._modules.default( 

                        type="event" 

                ).createEvent() 

 

                tabBar = self._tabWidget.tabBar() 

                closeButton = tabBar.tabButton(self._index, QtWidgets.QTabBar.RightSide) 

133                if not closeButton: 

                        #the mac os x case 

                        closeButton = tabBar.tabButton(self._index, QtWidgets.QTabBar.LeftSide) 

exit                closeButton.clicked.connect(lambda: self.closeRequested.send()) 

                closeButton.setShortcut(QtGui.QKeySequence.Close) 

 

        @property 

        def wrapperWidget(self): 

                return self._widget.wrapperWidget 

 

        @property 

        def _index(self): 

                return self._tabWidget.indexOf(self._widget.wrapperWidget) 

 

        def close(self): 

                self._tabWidget.removeTab(self._index) 

                if self._lastWidget: 

                        self._tabWidget.setCurrentWidget(self._lastWidget) 

 

exit        title = property( 

                lambda self: self._tabWidget.tabText(self._index), 

                lambda self, val: self._tabWidget.setTabText(self._index, val) 

        ) 

 

class LessonFileTab(FileTab): 

        def __init__(self, *args, **kwargs): 

                super().__init__(*args, **kwargs) 

 

                #properties are defined in parent class 

                self.tabChanged = self._modules.default(type="event").createEvent() 

exit                self._widget.currentChanged.connect(lambda: self.tabChanged.send()) 

 

        def retranslate(self): 

                """Called by the uiController module.""" 

                self._widget.retranslate() 

 

exit   exit        currentTab = property( 

                lambda self: self._widget.currentWidget(), 

                lambda self, value: self._widget.setCurrentWidget(value) 

        ) 

 

class GuiModule: 

        def __init__(self, moduleManager, *args, **kwargs): 

                super().__init__(*args, **kwargs) 

                self._mm = moduleManager 

 

                self.type = "ui" 

                self.requires = ( 

                        self._mm.mods(type="event"), 

                        self._mm.mods(type="startWidget"), 

                        self._mm.mods(type="metadata"), 

                ) 

                self.uses = ( 

                        self._mm.mods(type="buttonRegister"), 

                        self._mm.mods(type="translator"), 

                        self._mm.mods(type="settings"), 

                        # silences 'QtWebEngineWidgets must be imported before a 

                        # QCoreApplication instance is created' error message 

                        self._mm.mods(type='webEngine'), 

                ) 

                self.priorities = { 

                        "gtk": -1, 

                } 

                self.filesWithTranslations = ("gui.py", "ui.py") 

 

        def _msgHandler(self, type, ctx, message): 

                logFunc = { 

                        QtCore.QtDebugMsg: qtLogger.debug, 

                        QtCore.QtWarningMsg: qtLogger.warning, 

                        QtCore.QtCriticalMsg: qtLogger.critical, 

                        QtCore.QtFatalMsg: qtLogger.critical, 

                        QtCore.QtSystemMsg: qtLogger.critical, 

                }[type] 

                logFunc(message) 

 

        def enable(self): 

                warnings.warn("On Ubuntu, when going out of fullscreen mode, the native menu bar isn't restored due to a Unity bug. Remove that check when it's fixed from gui.py. Problem was still there 08/04/2017.") 

 

                global QtCore, QtGui, QtWidgets 

                try: 

                        from PyQt5 import QtCore, QtGui, QtWidgets 

                except ImportError as e: 

                        print(e) 

                        return 

 

                QtCore.qInstallMessageHandler(self._msgHandler) 

226                if hasattr(QtWidgets.QApplication, "x11EventFilter") and os.getenv("DISPLAY") is None: 

                        #if on a system that could potentially support X11, but 

                        #doesn't have it installed/running, leave this mod disabled. 

                        #Otherwise the whole application crashes on a 'Can't connect 

                        #to display' error message. 

                        # 

                        #checking for x11EventFilter because that is only defined 

                        #when Q_WS_X11 is set. No other way as far as I know to get 

                        #the value of that macro. :( 

                        return 

 

                #prevents that calling enable() and disable() multiple times 

                #segfaults. 

                self._app = QtWidgets.QApplication.instance() 

                if not self._app: 

                        self._app = QtWidgets.QApplication(sys.argv) 

 

                self._modules = set(self._mm.mods(type="modules")).pop() 

                createEvent = self._modules.default(type="event").createEvent 

 

                self.tabChanged = createEvent() 

                self.tabChanged.__doc__ = ( 

                        "This ``Event`` allows you to detect when the user " + 

                        "switches to another tab." 

                ) 

                self.applicationActivityChanged = createEvent() 

                self.applicationActivityChanged.__doc__ = ( 

                        "Handlers of this ``Event`` are called whenever the user " + 

                        "is switching between OpenTeacher and some other " + 

                        "program. They get one argument: ``'active'`` or " + 

                        "``'inactive'`` depending on if the user started to use " + 

                        "OpenTeacher or stopped using it." 

                ) 

 

                self._ui = self._mm.import_("ui") 

                self._ui.ICON_PATH = self._mm.resourcePath("icons/") 

 

                #try to load translations for Qt itself 

                qtTranslator = QtCore.QTranslator() 

                qtTranslator.load( 

                        "qt_" + QtCore.QLocale.system().name(), 

                        QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath) 

                ) 

                self._app.installTranslator(qtTranslator); 

 

                self._widget = self._ui.OpenTeacherWidget( 

                        self._modules.default("active", type="startWidget").createStartWidget(), 

                        self._onCloseRequested 

                ) 

 

                try: 

                        br = self._modules.default("active", type="buttonRegister") 

                except IndexError: 

                        pass 

                else: 

                        #add the open action as a load button too. 

                        self._loadButton = br.registerButton("load") 

exit                        self._loadButton.clicked.handle(lambda: self._widget.openAction.triggered.emit(False)) 

                        #always the load button first. 

                        self._loadButton.changePriority.send(0) 

                        self._loadButton.changeSize.send("small") 

 

                        #add a documentation button 

                        self._documentationButton = br.registerButton("help") 

exit                        self._documentationButton.clicked.handle(lambda: self._widget.docsAction.triggered.emit(False)) 

                        self._documentationButton.changeSize.send("small") 

 

                metadata = self._modules.default("active", type="metadata").metadata 

                self._widget.setWindowTitle(metadata["name"]) 

                self._widget.setWindowIcon(QtGui.QIcon(metadata["iconPath"])) 

 

                self._fileTabs = {} 

 

                #Make menus accessable 

                #file 

                self.fileMenu = Menu(createEvent, self._widget.fileMenu) 

                self.newAction = Action(createEvent, self._widget.fileMenu, self._widget.newAction) 

                self.openAction = Action(createEvent, self._widget.fileMenu, self._widget.openAction) 

                self.openIntoAction = Action(createEvent, self._widget.fileMenu, self._widget.openIntoAction) 

                self.saveAction = Action(createEvent, self._widget.fileMenu, self._widget.saveAction) 

                self.saveAsAction = Action(createEvent, self._widget.fileMenu, self._widget.saveAsAction) 

                self.printAction = Action(createEvent, self._widget.fileMenu, self._widget.printAction) 

                self.quitAction = Action(createEvent, self._widget.fileMenu, self._widget.quitAction) 

 

                #edit 

                self.editMenu = Menu(createEvent, self._widget.editMenu) 

                self.reverseAction = Action(createEvent, self._widget.editMenu, self._widget.reverseAction) 

                self.settingsAction = Action(createEvent, self._widget.editMenu, self._widget.settingsAction) 

 

                #view 

                self.viewMenu = Menu(createEvent, self._widget.viewMenu) 

                self.fullscreenAction = Action(createEvent, self._widget.viewMenu, self._widget.fullscreenAction) 

 

                #help 

                self.helpMenu = Menu(createEvent, self._widget.helpMenu) 

                self.documentationAction = Action(createEvent, self._widget.helpMenu, self._widget.docsAction) 

                self.aboutAction = Action(createEvent, self._widget.helpMenu, self._widget.aboutAction) 

 

                self._widget.tabWidget.currentChanged.connect(self._onTabChanged) 

exit                self._widget.activityChanged.connect( 

                        lambda activity: self.applicationActivityChanged.send( 

                                "active" if activity else "inactive" 

                        ) 

                ) 

 

                #make the statusViewer available 

                self.statusViewer = StatusViewer(self._widget.statusBar()) 

 

                #set application name (handy for e.g. Phonon) 

                self._app.setApplicationName(metadata["name"]) 

                self._app.setApplicationVersion(metadata["version"]) 

 

                #load translator 

                try: 

                        translator = self._modules.default("active", type="translator") 

                except IndexError: 

                        pass 

                else: 

                        translator.languageChanged.handle(self._retranslate) 

                self._retranslate() 

 

                self._addingTab = False 

 

                self.active = True 

 

        def _onTabChanged(self): 

                #when adding a tab, this triggers a bit too early. Because of 

                #that, it's called manually by the functions that add a tab. 

                if not self._addingTab: 

                        self.tabChanged.send() 

 

        def disable(self): 

                self.active = False 

 

                try: 

                        br = self._modules.default("active", type="buttonRegister") 

                except IndexError: 

                        pass 

                else: 

                        #we don't unhandle the event, since PyQt5 does some weird 

                        #memory stuff making it impossible to find the right item, 

                        #and it's unneeded anyway. 

                        br.unregisterButton(self._loadButton) 

                        del self._loadButton 

                        br.unregisterButton(self._documentationButton) 

                        del self._documentationButton 

 

                del self._modules 

                del self._ui 

                del self._fileTabs 

                del self._widget 

                del self._app 

 

                del self.tabChanged 

                del self.applicationActivityChanged 

 

                del self.fileMenu 

                del self.newAction 

                del self.openAction 

                del self.openIntoAction 

                del self.saveAction 

                del self.saveAsAction 

                del self.printAction 

                del self.quitAction 

 

                del self.editMenu 

                del self.reverseAction 

                del self.settingsAction 

 

                del self.viewMenu 

                del self.fullscreenAction 

 

                del self.helpMenu 

                del self.documentationAction 

                del self.aboutAction 

 

                del self.statusViewer 

 

                del self._addingTab 

 

        def _retranslate(self): 

                global _ 

                global ngettext 

 

                try: 

                        translator = self._modules.default("active", type="translator") 

                except IndexError: 

exit                        _, ngettext = str, lambda a, b, n: a if n == 1 else b 

                else: 

                        _, ngettext = translator.gettextFunctions( 

                                self._mm.resourcePath("translations") 

                        ) 

 

                self._ui._, self._ui.ngettext = _, ngettext 

                self._widget.retranslate() 

 

                for fileTab in self._fileTabs.values(): 

                        if fileTab.__class__ == LessonFileTab: 

                                fileTab.retranslate() 

 

                self._loadButton.changeText.send(_("Open from file")) 

                self._documentationButton.changeText.send(_("Documentation")) 

 

        def run(self, onCloseRequested): 

                """Starts the event loop of the Qt application. 

                   Can only be called once. 

 

                """ 

                self._closeCallback = onCloseRequested 

 

                self._widget.show() 

                self._app.exec_() 

 

        def _onCloseRequested(self): 

                #if not running, there's nothing that can be closed, so don't 

                #check if the method exists. (The callback is assigned in 

                #run().) 

                return self._closeCallback() 

 

        def interrupt(self): 

                """Closes all windows currently opened. (Including windows from 

                   other modules.) 

 

                """ 

                self._app.closeAllWindows() 

 

        def setFullscreen(self, bool): 

                """Enables or disables full screen depending on the ``bool`` 

                   argument. 

 

                """ 

                #native menubar enable/disable to keep it into view while 

                #fullscreen in at least unity. 

                if bool: 

                        self._widget.menuBar().setNativeMenuBar(False) 

                        self._widget.showFullScreen() 

                else: 

                        if platform.linux_distribution()[0] != "Ubuntu": 

                                #on Unity, we don't re-enable the native menu bar, 

                                #because a re-enabled native menu bar doesn't work ok. 

                                self._widget.menuBar().setNativeMenuBar(True) 

                        self._widget.showNormal() 

 

        @property 

        def startTab(self): 

                """Gives access to the start tab widget.""" 

 

                return self._widget.tabWidget.startWidget 

 

        def showStartTab(self): 

                """Changes the current tab to be the same as the one shown on 

                   application start. 

 

                """ 

                self._widget.tabWidget.setCurrentWidget(self._widget.tabWidget.startWidget.wrapperWidget) 

 

        def addFileTab(self, enterWidget=None, teachWidget=None, resultsWidget=None, previousTabOnClose=False): 

                """The same as ``addCustomTab``, except that it takes three 

                   widgets (one for enteringItems, on for teaching them and one 

                   for showing the teaching results) that are combined into a 

                   single tab. 

 

                """ 

                widget = self._ui.LessonTabWidget(enterWidget, teachWidget, resultsWidget) 

 

                return self.addCustomTab(widget, previousTabOnClose) 

 

        def addCustomTab(self, widget, previousTabOnClose=False): 

                """Adds ``widget`` as a tab in the main window. If 

                   ``previousTabOnClose`` is true, the currently visible tab is 

                   shown again when the created tab is closed. 

 

                """ 

 

492                if previousTabOnClose: 

                        lastWidget = self._widget.tabWidget.currentWidget() 

                else: 

                        lastWidget = None 

 

                self._addingTab = True 

                self._widget.tabWidget.addTab(widget, "") 

                self._addingTab = False 

 

                args = (self._mm, self._widget.tabWidget, widget, lastWidget) 

 

                if widget.__class__ == self._ui.LessonTabWidget: 

                        fileTab = LessonFileTab(*args) 

                else: 

                        fileTab = FileTab(*args) 

                self._fileTabs[widget.wrapperWidget] = fileTab 

 

                self._onTabChanged() 

                return fileTab 

 

        @property 

        def currentFileTab(self): 

                """Gives access to the currently shown file tab (if any, 

                   otherwise this returns ``None``.) 

 

                """ 

                try: 

                        return self._fileTabs[self._widget.tabWidget.currentWidget()] 

                except KeyError: 

                        return 

 

        @currentFileTab.setter 

        def currentFileTab(self, value): 

                #reverse dictionary lookup. 

                for widget, fileTab in self._fileTabs.items(): 

                        if fileTab == value: 

                                self._widget.tabWidget.setCurrentWidget(widget) 

 

        def addStyleSheetRules(self, rules): 

                """Adds global Qt style sheet rules to the current QApplication. 

                   An example use is to theme OpenTeacher. 

 

                """ 

                self._app.setStyleSheet(self._app.styleSheet() + "\n\n" + rules) 

 

        def setStyle(self, style): 

                """Allows you to set an app-wide QStyle. Handy for theming.""" 

 

                self._app.setStyle(style) 

 

        @property 

        def qtParent(self): 

                """Only use this as widget parent, or for application 

                global Qt settings, and don't be surprised if another 

                module sets that setting differently. 

 

                """ 

                return self._widget 

 

        @property 

        def startTabActive(self): 

                """Tells you if the start tab is active at the moment this 

                   property is accessed. 

 

                """ 

                return self._widget.tabWidget.startWidget.wrapperWidget == self._widget.tabWidget.currentWidget() 

 

def init(moduleManager): 

        return GuiModule(moduleManager)